Compare commits

...

275 Commits

Author SHA1 Message Date
Evgeny Poberezkin
75480eabda update rfc 2024-01-30 17:31:34 +00:00
Evgeny Poberezkin
fbc586a9da store images as blobs 2024-01-29 11:48:29 +00:00
Evgeny Poberezkin
af946b45c2 rfc: more space-efficient message encoding 2024-01-28 21:02:14 +00:00
Evgeny Poberezkin
bce829ef58 v5.5.1: ios 195, android 177, desktop 27 2024-01-27 23:23:29 +00:00
Evgeny Poberezkin
7df300cf36 core: 5.5.1.0 2024-01-27 19:18:41 +00:00
Stanislav Dmitrenko
9b35ff6f11 android, desktop: possible fix of chat items race (#3520)
* android, desktop: possible fix of chat items race

* fix compose threads desynchronization

* optimization and preventing race

* removed unused logs

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-01-27 18:46:27 +00:00
M. Sarmad Qadeer
c4cbb49f57 website: update contact page layout if JS is disabled (#3331)
* website: update page layout for the case if javascript is disabled in browser.

* website: add noscript tag & update heading

* website: do some changes in layout of contact page without JS

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-01-27 16:51:21 +00:00
Stanislav Dmitrenko
2b3eebb7a2 android, desktop: show different text when database migrates (#3762)
* android, desktop: show different text when database migrates

* one more check

* refactor

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-01-26 23:08:48 +00:00
Evgeny Poberezkin
a1328c287c core: remove unused events from api (#3764)
* core: remove unused events from api

* fix test
2024-01-26 18:54:08 +00:00
Stanislav Dmitrenko
f2d498dd79 android, desktop: removed timer for some long running jobs (#3761) 2024-01-26 18:15:20 +00:00
Stanislav Dmitrenko
6b8fc6fdcf android, desktop: lower limit of terminal items for non-developers (#3763) 2024-01-26 18:14:53 +00:00
spaced4ndy
0e585d5e5b core: fix group invitation marked deleted 2024-01-26 20:30:21 +04:00
Stanislav Dmitrenko
520d8868ef android: refactor clipboard access to prevent Android error in logs (#3758) 2024-01-26 14:00:53 +00:00
spaced4ndy
7192448303 core: fix invitation as rejected when deleting group (#3759) 2024-01-26 17:56:17 +04:00
spaced4ndy
0f0f65533a android: group welcome message byte limit (#3756)
* android: group welcome message byte limit

* text

* change footer
2024-01-26 09:57:04 +00:00
spaced4ndy
78a38cb080 ios: group welcome message byte limit (#3751)
* ios: group welcome message character limit

* confirmation dialogue key

* use chatJsonLength

* text

* change footer

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-01-26 09:37:49 +00:00
Stanislav Dmitrenko
f102f39147 android, desktop: protocol servers fix (#3755)
Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-01-25 20:59:02 +00:00
Stanislav Dmitrenko
cd349e80ce desktop: propertly updating delivery tab of chat item info page (#3752) 2024-01-25 17:51:20 +00:00
Stanislav Dmitrenko
430dc5bd2e ui: don't show context menu on non-sent yet live message (#3754)
* android, desktop: don't show context menu on non-sent yet live message

* ios: don't show context menu on non-sent yet live message

---------

Co-authored-by: Avently <avently@local>
2024-01-25 17:48:40 +00:00
spaced4ndy
3e0b863b64 core: add cChatJsonLength function (#3753) 2024-01-25 18:57:38 +04:00
Stanislav Dmitrenko
d6afee11bc desktop: prevent clicking enter on alert and text field at the same time (#3714) 2024-01-25 14:50:53 +00:00
spaced4ndy
6ef3a9e668 ios: fix welcome view (#3743)
* welcome view (still doesn't keep change on re-open)

* fix

* remove debug log

* rename
2024-01-25 16:08:10 +04:00
spaced4ndy
fbe3353434 ui: fix link preview cancellation (#3750) 2024-01-25 14:58:39 +04:00
Evgeny Poberezkin
d6d2e6e1eb Merge branch 'stable' 2024-01-24 19:55:28 +00:00
Evgeny Poberezkin
14d5078404 blog: v5.5 announcement (#3744)
* blog: v5.5 announcement

* update

* update

* readme

* images, update
2024-01-24 19:54:47 +00:00
Stanislav Dmitrenko
da1d20c17f desktop: alignment for reactions (#3747) 2024-01-24 16:25:44 +00:00
Stanislav Dmitrenko
afc324dc4f android, desktop: marking chat as read if it was set unread (#3746) 2024-01-24 16:24:49 +00:00
Stanislav Dmitrenko
f81e457e09 android: trying to start service again in case it was destroyed (#3745) 2024-01-24 16:22:29 +00:00
Stanislav Dmitrenko
bd30b80e15 desktop: custom time picker (#3741)
* desktop: custom time picker

* text color

* formatting

* changes in UI

* optimization

* desktop: opening SimpleX links inside the app (#3738)

* 5.5: ios 194, android 175, desktop 26

* docs: update downloads page

* ui: fix chat preview showing incorrect timestamp when chat is empty (#3739)

* ui: align call buttons with calls preference (#3740)

* ui: deleted item preview (#3726)

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
2024-01-24 16:15:27 +00:00
spaced4ndy
da9a7f4642 ui: exclude not ready and active contacts from list of contacts to add to group (e.g. simplex team contact) (#3737) 2024-01-24 13:56:38 +04:00
spaced4ndy
838a759a76 ui: deleted item preview (#3726) 2024-01-24 13:44:29 +04:00
spaced4ndy
f1ff27218c ui: align call buttons with calls preference (#3740) 2024-01-24 13:43:18 +04:00
spaced4ndy
8738cf332f ui: fix chat preview showing incorrect timestamp when chat is empty (#3739) 2024-01-24 13:37:29 +04:00
Evgeny Poberezkin
4f602d4571 docs: update downloads page 2024-01-23 23:22:00 +00:00
Evgeny Poberezkin
d0b75e0dd3 5.5: ios 194, android 175, desktop 26 2024-01-23 21:11:52 +00:00
Stanislav Dmitrenko
1bb98706a6 desktop: opening SimpleX links inside the app (#3738) 2024-01-23 17:05:19 +00:00
Evgeny Poberezkin
6b7c13f20a ui: translations (#3732)
* Translated using Weblate (Russian)

Currently translated at 100.0% (1605 of 1605 strings)

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

* Translated using Weblate (Russian)

Currently translated at 100.0% (1403 of 1403 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (1605 of 1605 strings)

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

* Translated using Weblate (Turkish)

Currently translated at 96.5% (1354 of 1403 strings)

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

* Translated using Weblate (Hungarian)

Currently translated at 100.0% (1605 of 1605 strings)

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

* Translated using Weblate (Hungarian)

Currently translated at 100.0% (1605 of 1605 strings)

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

* Translated using Weblate (Russian)

Currently translated at 100.0% (1605 of 1605 strings)

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

* Translated using Weblate (Russian)

Currently translated at 100.0% (1403 of 1403 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (1605 of 1605 strings)

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

* Translated using Weblate (Turkish)

Currently translated at 96.5% (1354 of 1403 strings)

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

* Translated using Weblate (Hungarian)

Currently translated at 100.0% (1605 of 1605 strings)

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

* Translated using Weblate (Hungarian)

Currently translated at 100.0% (1605 of 1605 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 99.1% (1592 of 1605 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 99.2% (1392 of 1403 strings)

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

* Added translation using Weblate (Romanian)

* Translated using Weblate (German)

Currently translated at 100.0% (1605 of 1605 strings)

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

* Translated using Weblate (German)

Currently translated at 100.0% (1403 of 1403 strings)

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (1605 of 1605 strings)

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (1403 of 1403 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 100.0% (1605 of 1605 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 100.0% (1403 of 1403 strings)

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

* Translated using Weblate (Hungarian)

Currently translated at 100.0% (1605 of 1605 strings)

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

* Translated using Weblate (Hungarian)

Currently translated at 100.0% (1605 of 1605 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 7.4% (119 of 1605 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 3.4% (56 of 1605 strings)

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

* Translated using Weblate (Romanian)

Currently translated at 4.6% (75 of 1605 strings)

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

* Translated using Weblate (Arabic)

Currently translated at 99.7% (1601 of 1605 strings)

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

* Translated using Weblate (Polish)

Currently translated at 100.0% (1605 of 1605 strings)

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

* Translated using Weblate (Polish)

Currently translated at 100.0% (1403 of 1403 strings)

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

* Translated using Weblate (Lithuanian)

Currently translated at 41.0% (659 of 1605 strings)

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

* ios: import/export localizations

* revert ar "blocked"

* corrections

---------

Co-authored-by: Artem Vorotnikov <artem@vorotnikov.me>
Co-authored-by: 大王叫我来巡山 <hamburger2048@users.noreply.hosted.weblate.org>
Co-authored-by: summoner001 <summoner@vivaldi.net>
Co-authored-by: Ghost of Sparta <makesocialfoss32@keemail.me>
Co-authored-by: M1K4 <oomikaoo@gmail.com>
Co-authored-by: mlanp <github@lang.xyz>
Co-authored-by: Random <random-r@users.noreply.hosted.weblate.org>
Co-authored-by: Siavash <ciavash@proton.me>
Co-authored-by: FalxDraco <acc.557q@goea.dev>
Co-authored-by: jonnysemon <jonnysemon@users.noreply.hosted.weblate.org>
Co-authored-by: B.O.S.S <BxOxSxS@protonmail.com>
Co-authored-by: Moo <hazap@hotmail.com>
2024-01-22 22:00:13 +00:00
Stanislav Dmitrenko
93d2bb98b5 android, desktop: small fixes to UI (#3731) 2024-01-22 19:45:39 +00:00
Evgeny Poberezkin
3b614e95e3 ios: update library (5.5.0.4) 2024-01-22 19:35:39 +00:00
spaced4ndy
1a9e942e33 core: 5.5.0.4 2024-01-22 18:35:47 +04:00
spaced4ndy
bddb9c14b7 core: update simplexmq 5.5.1.1 (optimize expired messages query) (#3730) 2024-01-22 18:33:48 +04:00
spaced4ndy
afd145c979 core: improve chat list performance (indexes) (#3728) 2024-01-22 18:03:40 +04:00
Stanislav Dmitrenko
97fbf2b7fe android, desktop: switching C calls to IO threads (#3727) 2024-01-22 12:56:20 +00:00
Evgeny Poberezkin
db93d29e76 ios: fix removing group link in iOS 17 (#3725) 2024-01-21 10:09:32 +00:00
Evgeny Poberezkin
10b3fe1390 5.5-beta.2: ios 191, android 174, desktop 25 2024-01-20 22:54:11 +00:00
Evgeny Poberezkin
a2b9a04430 core: 5.5.0.3 2024-01-20 21:35:28 +00:00
Evgeny Poberezkin
444b92282e Merge branch 'stable' 2024-01-20 21:34:30 +00:00
Evgeny Poberezkin
c35d275273 ci: fix windows desktop build (#3721) 2024-01-20 21:34:03 +00:00
Evgeny Poberezkin
052ef2037a core: update min version for remote to 5.5.0.2 (#3720) 2024-01-20 20:53:26 +00:00
Evgeny Poberezkin
0b731d818b Merge branch 'stable' 2024-01-20 20:12:39 +00:00
Stanislav Dmitrenko
1aab9f7f8a ci: fix windows build (#3719) 2024-01-20 20:12:01 +00:00
Evgeny Poberezkin
bdf3ac1a36 Merge branch 'stable' 2024-01-20 19:20:45 +00:00
Evgeny Poberezkin
8159ae4aea 5.4.4: ios 190, android 172, desktop 24 2024-01-20 18:21:22 +00:00
Evgeny Poberezkin
a9ba0a2e8a core: 5.5.0.2, update simplexmq 5.5.1.0 2024-01-20 15:02:03 +00:00
Evgeny Poberezkin
c65a17fccb core: output messages and events while executing the CLI command passed via -e option (#3683)
* core: output messages and events while executing the CLI command passed via -e option

* option

* add option
2024-01-20 14:59:13 +00:00
Evgeny Poberezkin
5aa4b7687e website: translations (#3718)
* Translated using Weblate (Arabic)

Currently translated at 100.0% (252 of 252 strings)

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (252 of 252 strings)

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

* Translated using Weblate (Arabic)

Currently translated at 100.0% (252 of 252 strings)

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (252 of 252 strings)

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

---------

Co-authored-by: jonnysemon <jonnysemon@users.noreply.hosted.weblate.org>
Co-authored-by: Random <random-r@users.noreply.hosted.weblate.org>
2024-01-20 13:48:45 +00:00
Evgeny Poberezkin
1016513dd3 ui: translations (#3717)
* Translated using Weblate (German)

Currently translated at 99.9% (1570 of 1571 strings)

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (1571 of 1571 strings)

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (1371 of 1371 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (1571 of 1571 strings)

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

* Translated using Weblate (Hungarian)

Currently translated at 100.0% (1571 of 1571 strings)

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

* Translated using Weblate (German)

Currently translated at 100.0% (1571 of 1571 strings)

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

* Translated using Weblate (German)

Currently translated at 100.0% (1371 of 1371 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 100.0% (1571 of 1571 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 100.0% (1371 of 1371 strings)

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

* Translated using Weblate (Hungarian)

Currently translated at 100.0% (1571 of 1571 strings)

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

* Translated using Weblate (Hungarian)

Currently translated at 100.0% (1571 of 1571 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (1571 of 1571 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (1371 of 1371 strings)

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

* Translated using Weblate (Japanese)

Currently translated at 99.0% (1556 of 1571 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 97.6% (1534 of 1571 strings)

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

* Translated using Weblate (Japanese)

Currently translated at 100.0% (1571 of 1571 strings)

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

* Added translation using Weblate (Persian)

* Translated using Weblate (Italian)

Currently translated at 100.0% (1571 of 1571 strings)

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (1371 of 1371 strings)

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

* Translated using Weblate (Japanese)

Currently translated at 100.0% (1571 of 1571 strings)

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

* Translated using Weblate (Bulgarian)

Currently translated at 99.8% (1569 of 1571 strings)

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

* Translated using Weblate (Japanese)

Currently translated at 100.0% (1571 of 1571 strings)

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

* Translated using Weblate (Japanese)

Currently translated at 100.0% (1571 of 1571 strings)

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

* Translated using Weblate (Japanese)

Currently translated at 100.0% (1571 of 1571 strings)

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

* Translated using Weblate (Japanese)

Currently translated at 100.0% (1571 of 1571 strings)

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

* Translated using Weblate (Bulgarian)

Currently translated at 100.0% (1371 of 1371 strings)

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

* Translated using Weblate (Bulgarian)

Currently translated at 100.0% (1571 of 1571 strings)

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

* Translated using Weblate (Japanese)

Currently translated at 100.0% (1571 of 1571 strings)

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

* Translated using Weblate (Persian)

Currently translated at 4.0% (63 of 1571 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 5.6% (88 of 1571 strings)

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

* Translated using Weblate (Arabic)

Currently translated at 99.7% (1574 of 1578 strings)

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

* Translated using Weblate (Hungarian)

Currently translated at 100.0% (1578 of 1578 strings)

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

* Translated using Weblate (Hungarian)

Currently translated at 100.0% (1583 of 1583 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 99.3% (1584 of 1594 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (1594 of 1594 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (1397 of 1397 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (1594 of 1594 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 100.0% (1594 of 1594 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 100.0% (1397 of 1397 strings)

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

* Translated using Weblate (Arabic)

Currently translated at 99.7% (1590 of 1594 strings)

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

* Translated using Weblate (Hungarian)

Currently translated at 100.0% (1594 of 1594 strings)

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

* Translated using Weblate (Hungarian)

Currently translated at 100.0% (1594 of 1594 strings)

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

* Translated using Weblate (Italian)

Currently translated at 99.5% (1587 of 1594 strings)

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

* Translated using Weblate (Italian)

Currently translated at 99.9% (1593 of 1594 strings)

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (1594 of 1594 strings)

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (1397 of 1397 strings)

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

* Translated using Weblate (German)

Currently translated at 99.9% (1570 of 1571 strings)

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (1571 of 1571 strings)

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (1371 of 1371 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (1571 of 1571 strings)

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

* Translated using Weblate (Hungarian)

Currently translated at 100.0% (1571 of 1571 strings)

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

* Translated using Weblate (German)

Currently translated at 100.0% (1571 of 1571 strings)

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

* Translated using Weblate (German)

Currently translated at 100.0% (1371 of 1371 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 100.0% (1571 of 1571 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 100.0% (1371 of 1371 strings)

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

* Translated using Weblate (Hungarian)

Currently translated at 100.0% (1571 of 1571 strings)

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

* Translated using Weblate (Hungarian)

Currently translated at 100.0% (1571 of 1571 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (1571 of 1571 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (1371 of 1371 strings)

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

* Translated using Weblate (Japanese)

Currently translated at 99.0% (1556 of 1571 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 97.6% (1534 of 1571 strings)

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

* Translated using Weblate (Japanese)

Currently translated at 100.0% (1571 of 1571 strings)

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

* Added translation using Weblate (Persian)

* Translated using Weblate (Italian)

Currently translated at 100.0% (1571 of 1571 strings)

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (1371 of 1371 strings)

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

* Translated using Weblate (Japanese)

Currently translated at 100.0% (1571 of 1571 strings)

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

* Translated using Weblate (Bulgarian)

Currently translated at 99.8% (1569 of 1571 strings)

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

* Translated using Weblate (Japanese)

Currently translated at 100.0% (1571 of 1571 strings)

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

* Translated using Weblate (Japanese)

Currently translated at 100.0% (1571 of 1571 strings)

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

* Translated using Weblate (Japanese)

Currently translated at 100.0% (1571 of 1571 strings)

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

* Translated using Weblate (Japanese)

Currently translated at 100.0% (1571 of 1571 strings)

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

* Translated using Weblate (Bulgarian)

Currently translated at 100.0% (1371 of 1371 strings)

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

* Translated using Weblate (Bulgarian)

Currently translated at 100.0% (1571 of 1571 strings)

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

* Translated using Weblate (Japanese)

Currently translated at 100.0% (1571 of 1571 strings)

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

* Translated using Weblate (Persian)

Currently translated at 4.0% (63 of 1571 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 5.6% (88 of 1571 strings)

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

* Translated using Weblate (Arabic)

Currently translated at 99.7% (1574 of 1578 strings)

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

* Translated using Weblate (Hungarian)

Currently translated at 100.0% (1578 of 1578 strings)

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

* Translated using Weblate (Hungarian)

Currently translated at 100.0% (1583 of 1583 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 99.3% (1584 of 1594 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (1594 of 1594 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (1397 of 1397 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (1594 of 1594 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 100.0% (1594 of 1594 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 100.0% (1397 of 1397 strings)

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

* Translated using Weblate (Arabic)

Currently translated at 99.7% (1590 of 1594 strings)

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

* Translated using Weblate (Hungarian)

Currently translated at 100.0% (1594 of 1594 strings)

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

* Translated using Weblate (Hungarian)

Currently translated at 100.0% (1594 of 1594 strings)

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

* Translated using Weblate (Italian)

Currently translated at 99.5% (1587 of 1594 strings)

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

* Translated using Weblate (Italian)

Currently translated at 99.9% (1593 of 1594 strings)

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (1594 of 1594 strings)

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (1397 of 1397 strings)

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

* Update translation files

Updated by "Cleanup translation files" hook in Weblate.

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

* Translated using Weblate (Russian)

Currently translated at 100.0% (1592 of 1592 strings)

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

* Translated using Weblate (Russian)

Currently translated at 99.8% (1395 of 1397 strings)

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

* android CDATA

* ios: import/export localizations

* update ja

* ios: fix directory service address in translations

* android: fix translations, CDATA

---------

Co-authored-by: mlanp <github@lang.xyz>
Co-authored-by: Random <random-r@users.noreply.hosted.weblate.org>
Co-authored-by: 大王叫我来巡山 <hamburger2048@users.noreply.hosted.weblate.org>
Co-authored-by: summoner001 <summoner@vivaldi.net>
Co-authored-by: M1K4 <oomikaoo@gmail.com>
Co-authored-by: Ghost of Sparta <makesocialfoss32@keemail.me>
Co-authored-by: Ophiushi <41908476+ishi-sama@users.noreply.github.com>
Co-authored-by: tomato potato <tomato.games@protonmail.com>
Co-authored-by: No name <CertainBot@users.noreply.hosted.weblate.org>
Co-authored-by: elgratea <weblate@fastmail.com>
Co-authored-by: Y. Sakamoto <ysakamoto@tutanota.com>
Co-authored-by: a4318 <dalse.077@gmail.com>
Co-authored-by: hakkadaikon <hakkadaikon@yahoo.co.jp>
Co-authored-by: Siavash <ciavash@proton.me>
Co-authored-by: jonnysemon <jonnysemon@users.noreply.hosted.weblate.org>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
2024-01-20 13:46:23 +00:00
spaced4ndy
106556812c android, desktop: block member for all (#3711)
* android: block member for all

* old buttons (revert this)

* blocked by admin text

* revise notification
2024-01-20 10:33:49 +00:00
spaced4ndy
7d2f6a3609 ios: block member for all (#3708)
* ios: block for all

* member info

* change conditions

* update ui

* blockedByAdmin

* fix

* rework

* fix, comment

* comment

* fix

* fix

* fixes for default

* fix text

* reorder

* fix

* fix

* secondary

* old buttons (revert this)

* blocked by admin text

* revise notification
2024-01-20 10:33:36 +00:00
Evgeny Poberezkin
f188118643 ui: update whats new (#3716) 2024-01-20 09:07:57 +00:00
Evgeny Poberezkin
cc05434b31 core: process message errors (#3709)
* core: process message errors

* update simplexmq commit sha

* style

Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>

* simplexmq

---------

Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
2024-01-20 08:17:57 +00:00
Evgeny Poberezkin
68de2b7540 core: update preset smp servers (#3713) 2024-01-19 19:50:21 +00:00
Stanislav Dmitrenko
5d8bb24d1c android, desktop: withLongRunningApi when needed (#3710) 2024-01-19 17:01:33 +00:00
Stanislav Dmitrenko
ab9a6dcab5 ios: allow to delete the last profile (#3707)
* ios: allow to delete the last profile

* less changes

* no flash of empty view

---------

Co-authored-by: Avently <avently@local>
2024-01-19 15:52:13 +00:00
spaced4ndy
0dd1f13d3b core: fix blocked by admin encoding 2024-01-19 19:14:35 +04:00
spaced4ndy
16f53490c5 core: block member for all (#3689)
* core: silence member wip

* change approach

* more types

* remove comment

* bool in protocol msg

* flag in items, event

* send event, process

* apply moderation

* remove comment

* filter for forward, view

* tests

* rename

* separate response, check already blocked

* add test

* fix terminal api

* add comment

* don't send profile update

* corrections

* refactor

* rework - flag blocked by admin

* blocked in intro

* fix test

* cant block unblock self

* muted view, create unknown member

* blocked by admin in members list

* create with maybe null

* protocol changes

* align code with protocol

* revert terminal api

* restrict block api for admins

* stabilize test

* rename

* update protocol/types

* refactor

* refactor2

* stabilize test

* member field

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-01-19 17:57:04 +04:00
Evgeny Poberezkin
4e502c4d13 Merge branch 'stable' 2024-01-18 21:00:59 +00:00
Evgeny Poberezkin
5236e0f201 core: 5.4.4.0 2024-01-18 20:56:45 +00:00
Evgeny Poberezkin
b69b72a93c ui: What's new in v5.5 (#3705)
* ios: whatsnew v5.5

* update

* android whats new, update ios

* export ios localizations
2024-01-18 20:44:57 +00:00
Stanislav Dmitrenko
2dae9180ec ios: text color of group invitation in chat list (#3703)
* ios: text color of group invitation in chat list

* refactor

---------

Co-authored-by: Avently <avently@local>
Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-01-18 19:14:18 +00:00
Stanislav Dmitrenko
ec57529f12 android, desktop: notes to self (#3695)
* android, desktop: notes to self

* change api

* icon

* icon

* icon

* eol

* icon

* changes

* color

* refactor

* color

* chats size

* size

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
2024-01-18 21:56:42 +04:00
Stanislav Dmitrenko
c4d75366b5 android, desktop: ability to delete the last profile (#3645)
* android, desktop: ability to delete the last profile

* hide modals to see onboarding after removing the last visible user

* removed unused checks

* make multiple profile creation usable

* deleting the last user by removing the whole database

* adapted to new logic

* changes

* not deleting the whole database

* refactor

* hide search when no profile

* changes
2024-01-18 21:16:44 +04:00
spaced4ndy
ddcd7d495e core: reset address and preferences when updating profile of member without associated contact (#3701) 2024-01-18 20:07:43 +04:00
Stanislav Dmitrenko
b5fe1f8364 ios: notes to self (#3690)
* ios: notes to self

* change

* icon

* changes

* no live message

* search

* alert

* better checks

* api change

* changes for review

* changes

* ios: align notes chat color with sent chat items frame color (#3704)

* changes

---------

Co-authored-by: Avently <avently@local>
Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
2024-01-18 19:57:14 +04:00
spaced4ndy
3c7e37ee9d android: profile updated chat items (#3700) 2024-01-18 15:43:26 +04:00
Evgeny Poberezkin
2884ad9fde readme: update link to users group 2024-01-17 22:47:41 +00:00
Evgeny Poberezkin
c130905efa docs: downloads version 2024-01-17 19:31:46 +00:00
Evgeny Poberezkin
915dc46bdc Merge branch 'stable' 2024-01-17 19:27:29 +00:00
Evgeny Poberezkin
1eb1ed92f7 ci: disable *-armv7a tags 2024-01-17 19:27:07 +00:00
Alexander Bondarenko
236daf4391 controller: add GetAgentWorkers/GetAgentWorkerDetails debug commands (#3681)
Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2024-01-17 17:37:29 +00:00
Evgeny Poberezkin
13feffb33a ios: update library 2024-01-17 15:44:03 +00:00
Evgeny Poberezkin
06d36751d7 Merge branch 'stable' 2024-01-17 15:25:29 +00:00
Evgeny Poberezkin
c25e9e3b72 scripts: curl/wget security (#3693) 2024-01-17 15:23:43 +00:00
Evgeny Poberezkin
868acd18d6 core: support deleting the last profile (always create user record in agent when user is created) (#3654)
* core: only skip creating agent user when app is first started

* firstTime

* prompt

* fix test

* if

* simplexmq

* ci timeout

* skip test

* add test timeout

* log test times

* simplexmq

* timeout

* simplexmq without new batching

* Revert "simplexmq without new batching"

This reverts commit 9879bcb57c.

* without new batching again

* with builder, without sized builder

* lazy bytestring, same batching logic

* fewer chunks

* remove lazy

* optimize batching in simplexmq
2024-01-17 15:20:13 +00:00
spaced4ndy
8c1114648a core: fix 8.10.7 compilation error (#3698)
* core: fix 8.10.7 compilation error

* fix
2024-01-17 13:47:19 +00:00
Stanislav Dmitrenko
3918c5306d android, desktop: disabling user picker items when chat is stopped (#3696) 2024-01-17 12:51:26 +00:00
Stanislav Dmitrenko
ab8a87acad android, desktop: fix alerts/modals when they are shown before UI init (#3697) 2024-01-17 12:50:48 +00:00
spaced4ndy
809dd1ef01 ios: profile updated chat items (#3692)
* ios: profile updated chat items

* remove vars

* refactor

* refactor

* refactor

* refactor

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2024-01-17 16:16:15 +04:00
Evgeny Poberezkin
b874cd1910 Merge branch 'stable' 2024-01-16 23:43:18 +00:00
Evgeny Poberezkin
300223b32e core: update simplexmq 5.5.0.6 (fix race conditions) (#3691)
* core: update simplexmq (fix race conditions)

* simplexmq 5.5.0.6
2024-01-16 23:42:29 +00:00
Stanislav Dmitrenko
88640b85c4 ios: local video encryption (#3682)
* ios: local video encryption

* main thread

* new progress view

* simplify

* rename

---------

Co-authored-by: Avently <avently@local>
Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2024-01-16 11:49:44 +00:00
spaced4ndy
d5cf9fbf5b core: exclude some fields from member profile when sharing in group (#3688) 2024-01-15 19:58:09 +04:00
spaced4ndy
f4f8501eb8 core: members profile update, create profile update chat items (#3644) 2024-01-15 19:56:11 +04:00
Stanislav Dmitrenko
be0c791c43 android, desktop: local video encryption (#3678)
* android, desktop: local video encryption

* refactor

* different look of progress indicator

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2024-01-15 15:00:57 +00:00
Stanislav Dmitrenko
46fe0fc671 withLongRunningApi (#3675) 2024-01-15 14:29:11 +00:00
Evgeny Poberezkin
bfb274b037 Merge branch 'stable' 2024-01-15 13:52:09 +00:00
Evgeny Poberezkin
8d7dcb550a core: update simplexmq, optimize batching, remove builder (#3685)
* core: update simplexmq (optimize batching, remove builder)

* do not use builder to batch

* refactor
2024-01-15 10:46:13 +00:00
spaced4ndy
135e735dd4 ui: show observer role in list of members (#3679) 2024-01-12 20:54:51 +04:00
spaced4ndy
516410a899 android, desktop: align member name functions with ios (fixes issue with displaying unknown members) (#3680) 2024-01-12 20:54:26 +04:00
Stanislav Dmitrenko
dad9716915 android, desktop: moving to single thread in api calls (#3670)
* android, desktop: moving to single thread in api calls

* more places

* more changes

* seconds

* long running api into init function

* changes

* developer options

* progress indicator

* string

* rename

* progressIndicator for stop chat
2024-01-11 18:50:25 +00:00
Alexander Bondarenko
bc8a6f4833 core: add notes chat type (#3568)
* Add chat type "self"

* rename to Notes

* cover more things

* remove quote, tweak sql

* resolve comments

* constrain ACIQDirection to exclude CTLocal

* add CILocalRcv handling

* plug in migrations and tests

* cover more API, implement new folders

* working create/send/tail

* remove interaction with messages

* add note deletion (api-only)

* add folder deletion

* add getLocalChatItemIdByText

* add APICreateChatItem and files

* add protocol check for getFileTransfer protocol

* replace FTLocal with createLocalFile

* add chat previews

* add folder clear

* add reactions

* add read/unread

* add note updates

* resolve some comments

* remove local reactions

* remove folder names, deletion, add autocreate

* add file deletion check

* add preview pagination test

* add per-item file deletion check

* pull mkChatItem out of createLocal to prevent ci record updates

* use - as notes name

* bump migration ts

* update schema

* resolve comments

* add chat pagination test

* use chat queries from Direct instead

* evict note folders from createUserRecord

* switch to - for note folder chat type prefix and use empty name

* fix getLocalChatXxx

* add explicit createCCNoteFolder for tests

* use overloadedstrings for single-line queries

* add suggested chat list tests

* add notes chat to a user-creating test

* throw correct error for missing file

* remove unique check from schema

* add UndecidableInstances for ghc8.10

* switch to * for chat type sigil

* add file safety test

* add drop index

* remove indentation

* remove repeated folder

* remove redundant filter query, NoteFolderName

* don't attempt to cancel local files when deleting chat item

* rename function

* fix comment

* rename

* fix merge

* fix typo

* remove editable limit

* restore comment

* remove local file cancel

* Revert "remove editable limit"

This reverts commit 65df55caf8.

* refactor

---------

Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
2024-01-11 17:01:44 +00:00
spaced4ndy
5b7a09f488 ui: fix unknown member UI (#3672) 2024-01-11 20:21:58 +04:00
spaced4ndy
bfe5d51df7 core, ui: create dummy member record when admin forwards a message from an unknown member (#3651)
* core: create dummy member record when admin forwards a message from an unknown member

* comments

* update unknown member if announced

* change removed

* change unknown name, revert diff

* revert diff

* ios

* update ios library

* android

* remove changes in iOS project file

* rename event

* remove unknown category

* android

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2024-01-11 17:55:13 +04:00
Evgeny Poberezkin
d9d270f00e ui: add Hungarian (Android only) and Turkish (#3671)
* ui: add Hungarian (Android only) and Turkish

* readme
2024-01-11 13:28:47 +00:00
Evgeny Poberezkin
2872b30e8c ui: translations (#3666)
* Translated using Weblate (German)

Currently translated at 100.0% (1510 of 1510 strings)

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

* Translated using Weblate (German)

Currently translated at 100.0% (1352 of 1352 strings)

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (1510 of 1510 strings)

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (1352 of 1352 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (1510 of 1510 strings)

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

* Translated using Weblate (Czech)

Currently translated at 98.5% (1488 of 1510 strings)

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

* Translated using Weblate (Czech)

Currently translated at 91.4% (1236 of 1352 strings)

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

* Translated using Weblate (Polish)

Currently translated at 100.0% (1510 of 1510 strings)

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

* Translated using Weblate (Polish)

Currently translated at 100.0% (1352 of 1352 strings)

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

* Translated using Weblate (Hungarian)

Currently translated at 99.9% (1509 of 1510 strings)

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

* Translated using Weblate (Hungarian)

Currently translated at 99.9% (1509 of 1510 strings)

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

* Translated using Weblate (Hungarian)

Currently translated at 99.9% (1509 of 1510 strings)

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

* Translated using Weblate (Hungarian)

Currently translated at 100.0% (1510 of 1510 strings)

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

* Translated using Weblate (Hungarian)

Currently translated at 100.0% (1510 of 1510 strings)

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

* Translated using Weblate (Polish)

Currently translated at 100.0% (1364 of 1364 strings)

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

* Translated using Weblate (Hungarian)

Currently translated at 100.0% (1510 of 1510 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (1534 of 1534 strings)

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

* Translated using Weblate (German)

Currently translated at 100.0% (1534 of 1534 strings)

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

* Translated using Weblate (German)

Currently translated at 100.0% (1364 of 1364 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 97.3% (1494 of 1534 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 97.5% (1330 of 1364 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 100.0% (1534 of 1534 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 100.0% (1364 of 1364 strings)

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

* Translated using Weblate (Polish)

Currently translated at 100.0% (1534 of 1534 strings)

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

* Translated using Weblate (Greek)

Currently translated at 14.7% (227 of 1534 strings)

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

* Translated using Weblate (Greek)

Currently translated at 1.6% (23 of 1364 strings)

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

* Translated using Weblate (German)

Currently translated at 100.0% (1534 of 1534 strings)

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

* Translated using Weblate (Russian)

Currently translated at 97.5% (1497 of 1534 strings)

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

* Translated using Weblate (German)

Currently translated at 100.0% (1364 of 1364 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (1534 of 1534 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (1364 of 1364 strings)

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (1534 of 1534 strings)

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (1364 of 1364 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 100.0% (1534 of 1534 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 100.0% (1364 of 1364 strings)

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

* Translated using Weblate (Japanese)

Currently translated at 88.5% (1208 of 1364 strings)

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

* Translated using Weblate (Japanese)

Currently translated at 90.3% (1386 of 1534 strings)

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

* Translated using Weblate (Arabic)

Currently translated at 99.7% (1530 of 1534 strings)

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

* Translated using Weblate (Bulgarian)

Currently translated at 91.9% (1254 of 1364 strings)

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

* Translated using Weblate (Bulgarian)

Currently translated at 100.0% (1534 of 1534 strings)

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

* Translated using Weblate (Japanese)

Currently translated at 91.0% (1399 of 1536 strings)

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

* Translated using Weblate (Japanese)

Currently translated at 92.3% (1418 of 1536 strings)

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

* Translated using Weblate (German)

Currently translated at 100.0% (1536 of 1536 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (1536 of 1536 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (1364 of 1364 strings)

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

* Translated using Weblate (Japanese)

Currently translated at 88.5% (1208 of 1364 strings)

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

* Translated using Weblate (Japanese)

Currently translated at 92.7% (1424 of 1536 strings)

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (1536 of 1536 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (1536 of 1536 strings)

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

* Translated using Weblate (Japanese)

Currently translated at 100.0% (1536 of 1536 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 36.8% (503 of 1364 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% (1536 of 1536 strings)

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

* Translated using Weblate (Turkish)

Currently translated at 58.8% (803 of 1364 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% (1536 of 1536 strings)

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

* Translated using Weblate (Turkish)

Currently translated at 64.0% (873 of 1364 strings)

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

* Translated using Weblate (Hungarian)

Currently translated at 97.9% (1505 of 1536 strings)

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

* Translated using Weblate (Hungarian)

Currently translated at 100.0% (1536 of 1536 strings)

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

* Translated using Weblate (Hungarian)

Currently translated at 100.0% (1536 of 1536 strings)

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

* Translated using Weblate (Hungarian)

Currently translated at 100.0% (1536 of 1536 strings)

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

* Translated using Weblate (Hungarian)

Currently translated at 100.0% (1536 of 1536 strings)

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

* Translated using Weblate (Hungarian)

Currently translated at 100.0% (1536 of 1536 strings)

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

* Translated using Weblate (Turkish)

Currently translated at 86.1% (1175 of 1364 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% (1536 of 1536 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% (1536 of 1536 strings)

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

* Translated using Weblate (Hungarian)

Currently translated at 100.0% (1536 of 1536 strings)

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

* Translated using Weblate (Turkish)

Currently translated at 87.4% (1193 of 1364 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% (1536 of 1536 strings)

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

* Translated using Weblate (Turkish)

Currently translated at 100.0% (1364 of 1364 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% (1536 of 1536 strings)

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

* Translated using Weblate (Czech)

Currently translated at 97.9% (1504 of 1536 strings)

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

* Translated using Weblate (Arabic)

Currently translated at 99.7% (1532 of 1536 strings)

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

* Translated using Weblate (Bulgarian)

Currently translated at 100.0% (1364 of 1364 strings)

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

* Translated using Weblate (Bulgarian)

Currently translated at 100.0% (1536 of 1536 strings)

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

* Translated using Weblate (Hungarian)

Currently translated at 100.0% (1536 of 1536 strings)

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (1542 of 1542 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (1542 of 1542 strings)

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

* Translated using Weblate (Arabic)

Currently translated at 99.7% (1538 of 1542 strings)

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

* Translated using Weblate (Hungarian)

Currently translated at 99.2% (1549 of 1560 strings)

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

* Translated using Weblate (German)

Currently translated at 100.0% (1562 of 1562 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (1562 of 1562 strings)

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

* fixing

* fix

* change

* fix

* import/export

* restore some lost translations

* 24hours

* Revert "24hours"

This reverts commit b715d46e6f.

* 24 hours, import

---------

Co-authored-by: mlanp <github@lang.xyz>
Co-authored-by: Random <random-r@users.noreply.hosted.weblate.org>
Co-authored-by: Eric <zxmegaxqug@hldrive.com>
Co-authored-by: zenobit <zen@osowoso.xyz>
Co-authored-by: B.O.S.S <BxOxSxS@protonmail.com>
Co-authored-by: J R <jr@simplex.chat>
Co-authored-by: summoner001 <summoner@vivaldi.net>
Co-authored-by: No name <CertainBot@users.noreply.hosted.weblate.org>
Co-authored-by: diodepon <diopon@mailo.com>
Co-authored-by: v1s7 <v1s7@users.noreply.hosted.weblate.org>
Co-authored-by: Ophiushi <41908476+ishi-sama@users.noreply.github.com>
Co-authored-by: M1K4 <oomikaoo@gmail.com>
Co-authored-by: 小林照幸 <koba1014@gmail.com>
Co-authored-by: jonnysemon <jonnysemon@users.noreply.hosted.weblate.org>
Co-authored-by: elgratea <weblate@fastmail.com>
Co-authored-by: yokoba0413 <yokoba0413@gmail.com>
Co-authored-by: Kaan P <kaanpeker196@gmail.com>
Co-authored-by: Ghost of Sparta <makesocialfoss32@keemail.me>
Co-authored-by: Avently <7953703+avently@users.noreply.github.com>
Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
2024-01-11 10:14:27 +00:00
Evgeny Poberezkin
f45b24367c cli: short command 2024-01-11 08:21:02 +00:00
Stanislav Dmitrenko
9e369c3ac9 android, desktop: lock on changing user (#3669)
* android, desktop: lock on changing user

* rename

* change

* change
2024-01-10 21:31:16 +00:00
Evgeny Poberezkin
afbb3b4e4b website: translations (#3667)
* Translated using Weblate (Arabic)

Currently translated at 100.0% (252 of 252 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (252 of 252 strings)

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

* Translated using Weblate (Japanese)

Currently translated at 100.0% (252 of 252 strings)

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

* correction

* correction

* revert

* keep new, revert changes in old ja strings

---------

Co-authored-by: jonnysemon <jonnysemon@users.noreply.hosted.weblate.org>
Co-authored-by: Eric <zxmegaxqug@hldrive.com>
Co-authored-by: yokoba0413 <yokoba0413@gmail.com>
2024-01-10 20:35:59 +00:00
Stanislav Dmitrenko
acd05c43db mobile: chat deletion avoiding race conditions (#3650)
* android, desktop: chat items deletion

* rename

* ios: chat items deletion

* correct id

* android: adding progress of deletion

* ios: text color while deleting chats

* change only text color

---------

Co-authored-by: Avently <avently@local>
Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2024-01-10 16:57:34 +00:00
Evgeny Poberezkin
61b14b22d5 5.5-beta.1: ios 189, android 171, desktop 23 2024-01-10 14:18:14 +00:00
Evgeny Poberezkin
25a4719414 core: 5.5.0.1 2024-01-10 12:06:29 +00:00
Evgeny Poberezkin
f802ec75fe Merge branch 'stable' 2024-01-10 12:05:00 +00:00
Evgeny Poberezkin
045b195483 5.4.3: ios 188, android 169, desktop 22 2024-01-10 11:56:57 +00:00
spaced4ndy
283c90f5ae core: fix db method reserving extra local display name (#3659)
Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2024-01-10 11:09:09 +04:00
Stanislav Dmitrenko
99a9fb2e1f ios: self destruct improvements (#3640)
* ios: self destruct improvements

* test

* adapted to stopped chat

* wait until ctrl initialization finishes

* Revert "test"

This reverts commit 7c199293cc.

* refactor

* simplify,fix

* refactor2

* refactor3

* comment

* fix

* fix

* comment

Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>

* flip and rename flag

---------

Co-authored-by: Avently <avently@local>
Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
2024-01-09 21:01:41 +00:00
Evgeny Poberezkin
ce9d583b39 Merge branch 'stable' 2024-01-09 20:36:30 +00:00
Evgeny Poberezkin
53414608db core: 5.4.3.0 (simplexmq 5.5.0.5) 2024-01-09 20:20:14 +00:00
Stanislav Dmitrenko
c7cf206585 android: fix call sound when the app in the background (#3660)
* android: fix call sound when the app in the background

* using previous notification channel

* Revert "using previous notification channel"

This reverts commit 19da9a9ce193c39b353f478e884a97bbbf002e77.

* prevent playing sound on call twice
2024-01-09 19:45:46 +00:00
Evgeny Poberezkin
6067ac3c93 ios: more aggressive GC in NSE 2024-01-09 19:34:54 +00:00
Evgeny Poberezkin
fc56873f1c ios: update core library 2024-01-09 19:33:55 +00:00
Stanislav Dmitrenko
a30da38af7 android, desktop: accept calls after restart (#3662) 2024-01-09 19:31:01 +00:00
Stanislav Dmitrenko
0bf3d054c6 mobile, desktop: invalid display name alert (#3664)
* ios: invalid display name alert

* android, desktop: invalid display name

---------

Co-authored-by: Avently <avently@local>
2024-01-09 19:26:47 +00:00
Stanislav Dmitrenko
a55a8b116a script: changes in script for downloading libs (#3663)
* script: changes in script for downloading libs

* ios script
2024-01-09 17:23:20 +00:00
Stanislav Dmitrenko
7a207fd641 android, desktop: alerts when device was disconnected (#3483) 2024-01-09 14:21:29 +00:00
Evgeny Poberezkin
4508e0dfc1 Merge branch 'stable' 2024-01-09 11:07:04 +00:00
Stanislav Dmitrenko
a853ba3a15 android, desktop: adapted code for self destruct for ios logic (#3643)
* android, desktop: adapted code for self destruct for ios logic

* init db in case of periodic && self destruct enabled
2024-01-09 09:59:21 +00:00
Evgeny Poberezkin
a2f190a6c6 core: update simplexmq (better batching) 2024-01-09 09:15:35 +00:00
Stanislav Dmitrenko
267178dddb android, desktop: show alerts on critical and internal errors (#3653)
* android, desktop: show alerts on critical and internal errors

* test

* don't stop chat if it's stopped already

* show notification

* restart chat or app

* Revert "test"

This reverts commit 5b78bbae5b.

* update strings

* strings2

* refactoring

* refactoring2

* refactoring3

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2024-01-08 18:20:52 +00:00
spaced4ndy
fadce0c140 core: create new chat controller with chatActivated set to true 2024-01-08 17:34:10 +04:00
spaced4ndy
58ad97fe6d core: pause cleanup when chat is suspended (#3658) 2024-01-08 17:28:01 +04:00
Evgeny Poberezkin
3ccd9903a7 core: do not start clean up manager in background NSE (#3657)
* core: do not start clean up manager in background NSE

* update UIs

* fix test
2024-01-08 12:53:16 +00:00
Evgeny Poberezkin
e294999044 ios: fix callkit calls via NSE (#3655)
* ios: fix callkit calls via NSE

* comments

* more reliable NSE start

* remove public logs, different RTS parameters for NSE

* only suspend NSE if we have chat controller, to avoid crashes if suspension attempted without controller created

* comments

* fix

* simplify
2024-01-08 10:56:01 +00:00
Evgeny Poberezkin
2bbc687f4a core: simplexmq 5.5.0.4 2024-01-06 11:48:28 +00:00
Evgeny Poberezkin
61c507e7da core: replace deprecated memcpy (#3652) 2024-01-06 11:32:26 +00:00
Evgeny Poberezkin
64230f3545 Merge branch 'stable' 2024-01-05 20:07:35 +00:00
Evgeny Poberezkin
bb61b9c658 core: update simplexmq (critical errors, worker restarts, subscription timeouts) 2024-01-05 20:07:19 +00:00
Evgeny Poberezkin
3428f4d2ee core: update simplexmq (critical errors, worker restarts, subscription timeouts) 2024-01-05 18:51:18 +00:00
Stanislav Dmitrenko
fe865c5e11 android, desktop: consistent colors in themes (#3649) 2024-01-05 18:45:52 +00:00
spaced4ndy
9e87fe73a5 core: batch send profile update (#3618)
* core: batch send profile update

* redundant

* reorder

* remove type

* createSndMessages

* refactor

* batched create internal item

* create feature items for multiple contacts

* comments

* refactor call site

* synonim

* refactor createSndMessages

* more batching

* remove partitionWith

* unite filter and fold

* refactor

* refactor

* refactor

* fix merge

* add test

* rename

* refactor

* refactor

* withExceptT

* refactor

* refactor2

* remove notChanged

* deliver with sendMessagesB (#3646)

* deliver with sendMessagesB

* refactor

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
Co-authored-by: Alexander Bondarenko <486682+dpwiz@users.noreply.github.com>
2024-01-05 11:35:48 +04:00
Evgeny Poberezkin
0ef2c55983 core: invalid name error when it matches hidden profile (#3647) 2024-01-04 19:29:28 +00:00
spaced4ndy
8882284fb7 core: always check integrity on MSG in direct chats (#3641) 2024-01-04 16:22:16 +04:00
Stanislav Dmitrenko
767522e701 ios: better way of starting chat after stop (#3637)
Co-authored-by: Avently <avently@local>
2024-01-02 20:20:05 +00:00
sh
575d899f5a build-android: fix new arrangement of nix command (#3634) 2024-01-02 14:39:23 +00:00
Stanislav Dmitrenko
d009777901 android, desktop: close gallery when media was deleted (#3636) 2024-01-02 14:38:28 +00:00
Stanislav Dmitrenko
f758a5526a android, desktop: specifying text color globally (#3635)
* android, desktop: specifying text color globally

* typography
2024-01-02 14:28:36 +00:00
Stanislav Dmitrenko
e6b5727003 android, desktop: run with stopped chat (#3624)
* android, desktop: run with stopped chat

* way to prevent starting a chat in case of not saved database key

* rename

* change position of a call

* new way of doing the same

* better

* exit process

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2024-01-02 14:21:39 +00:00
Evgeny Poberezkin
c9b1d54f13 docs: update downloads 2023-12-30 22:04:01 +00:00
Evgeny Poberezkin
05065e919b 5.5-beta.0: ios 187, android 168, desktop 21 2023-12-30 21:09:01 +00:00
Evgeny Poberezkin
5399212e48 core: 5.5.0.0 2023-12-30 18:59:00 +00:00
Evgeny Poberezkin
809040c7bc ui: show secrets on tap (#3628)
* ios: show secrets on tap

* android: show secrets on tap/click

* android: clickable links in group descriptions

* android: hide secrets one by one

* ios: clickable links in welcome message preview

* refactor

* refactor2
2023-12-30 18:57:10 +00:00
Evgeny Poberezkin
4ab078bd18 ios: show clear search button when search is not empty (#3627) 2023-12-30 14:09:07 +00:00
sh
825257e898 build-android: update nix and add armv7a branch switch (#3612)
* build-android: build armv7a in seperate tag

And update nix install.

* build-android: change tag detection logic

* build-android: minor change of logic
2023-12-30 10:19:56 +00:00
Evgeny Poberezkin
644169b835 cli: prompt for database key entry if required (#3626) 2023-12-30 10:02:55 +00:00
Stanislav Dmitrenko
78eefee6cc android, desktop: search view will be shown always (#3625)
* android, desktop: search view will be shown always

* rearrange tree

* optimization
2023-12-29 21:28:11 +00:00
Alexander Bondarenko
e253c55ba4 core: compatibility with GHC 8.10.7 (#3608)
* GHC-8.10 compatibility

* tweak setters

* restore membership

* remove Show Batch

* fix bytestring-10 compat

* preserve membership qualifier in names

* a few more memberships

* rename

* remove with-compiler

* ci: add 8.10 builds, limit releases to 9.6

* use matrix.asset_name as release guard

* fix windows_build

* actually use ghc version from matrix

* fix typo

* revert build/hash split

* add ghc to cache key

* Force cache between build and tests

* use explicit caching steps

* skip unneeded tasks

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
Co-authored-by: Avently <7953703+avently@users.noreply.github.com>
2023-12-29 21:15:14 +00:00
spaced4ndy
478bb32cdb core: send group description to new members as welcome message after sending history (fixes welcome message being created before history) (#3623) 2023-12-29 22:42:55 +04:00
Stanislav Dmitrenko
1438fd00e2 android, desktop: self destruct becomes better (#3598)
* android, desktop: self destruct becomes better

* better way of doing it

* fix script

* firstOrNull

* changes for review

* comment

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-12-29 17:47:25 +00:00
spaced4ndy
05b55d3fb5 ui: group history preference, enable in new groups by default; core: create group history feature items (#3596)
* Revert "core: do not create group history item (#3586)"

This reverts commit 2834b192ce.

* ios: group history preference

* fix tests

* android

* texts

* enable in new groups ios

* enable in new groups android

* android texts

* ios texts

* remove ellipsis

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-12-29 17:46:28 +00:00
Stanislav Dmitrenko
9c061508a4 android, desktop: rework UX of creating new connection (#3529)
* android, desktop: rework UX of creating new connection

* different place for clipboard listener

* changes

* changes

* changes

* button, strings

* focus

* code optimization and search icon

* incognito link

* comment

* paddings

* optimization

* icon size and space in search

* appbar background color

* secondary color for icons in search bar

* lighter tool bar and avatars

* darker avatars in toolbar

* background for selected item and divider

* replacing connection view with actual chat view

* clear

* close unneeded view

* filter icon background

* filter doesn't hide current chat with empty search field

* fixes for review

* clearing focus on hiding keyboard

* fix invalid qr code message

* rename

* color

* buttons and text visibility when chat is not running yet

* loading chats label

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
2023-12-29 15:46:45 +00:00
spaced4ndy
2bacc00a06 ios: rework UX of creating new connection (#3482)
* ios: connection UI (wip)

* custom search

* rework invite

* connect paste link ui

* scan rework, process errors, other fixes

* scan layout

* clear link on cancel

* improved search

* further improve search

* animation

* connect on paste in search

* layout

* layout

* layout

* layout, add conn

* delete unused invitation, create used invitation chat

* remove old views

* regular paste button

* new chat menu

* previews

* increase spacing

* animation, fix alerts

* swipe

* change text

* less sensitive gesture

* layout

* search cancel button transition

* slow down chat list animation (uses deprecated modifiers)

* icons

* update code scanner, layout

* manage camera permissions

* ask to delete unused invitation

* comment

* remove onDismiss

* don't filter chats on link in search, allow to paste text with link

* cleanup link after connection

* filter chat by link

* revert change

* show link descr

* disabled search

* underline

* filter own group

* simplify

* no animation

* add delay, move createInvitation

* update library

* possible fix for ios 15

* add explicit frame to qr code

* update library

* Revert "add explicit frame to qr code"

This reverts commit 95c7d31e47.

* remove comment

* fix pasteboardHasURLs, disable paste button based on it

* align help texts with changed button names

Co-authored-by: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com>

* update library

* Revert "fix pasteboardHasURLs, disable paste button based on it"

This reverts commit 46f63572e9.

* remove unused var

* restore disabled

* export localizations

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
Co-authored-by: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com>
2023-12-29 12:29:49 +00:00
Evgeny Poberezkin
d637714963 website: translations (#3617)
* Translated using Weblate (Ukrainian)

Currently translated at 100.0% (252 of 252 strings)

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

* Translated using Weblate (Ukrainian)

Currently translated at 100.0% (252 of 252 strings)

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

* Translated using Weblate (Ukrainian)

Currently translated at 100.0% (252 of 252 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 100.0% (252 of 252 strings)

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

---------

Co-authored-by: Maksym Lukashenko <livelmaxim@gmail.com>
Co-authored-by: No name <CertainBot@users.noreply.hosted.weblate.org>
2023-12-28 17:52:20 +00:00
Evgeny Poberezkin
5ff6bd15f6 ui: translations (#3615)
* Translated using Weblate (Italian)

Currently translated at 100.0% (1500 of 1500 strings)

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (1346 of 1346 strings)

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

* Translated using Weblate (Chinese (Traditional))

Currently translated at 82.9% (1116 of 1346 strings)

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

* Translated using Weblate (Turkish)

Currently translated at 9.2% (125 of 1346 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 80.1% (1202 of 1500 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 38.0% (571 of 1500 strings)

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

* Update translation files

Updated by "Remove blank strings" hook in Weblate.

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

* Translated using Weblate (Ukrainian)

Currently translated at 100.0% (1500 of 1500 strings)

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

* Translated using Weblate (Ukrainian)

Currently translated at 100.0% (1500 of 1500 strings)

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

* Translated using Weblate (Ukrainian)

Currently translated at 93.0% (1252 of 1346 strings)

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

* Translated using Weblate (Arabic)

Currently translated at 94.4% (1416 of 1500 strings)

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

* Translated using Weblate (Hungarian)

Currently translated at 41.2% (619 of 1500 strings)

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (1500 of 1500 strings)

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (1346 of 1346 strings)

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

* Translated using Weblate (Arabic)

Currently translated at 99.7% (1496 of 1500 strings)

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (1500 of 1500 strings)

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (1346 of 1346 strings)

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

* Translated using Weblate (Japanese)

Currently translated at 90.9% (1224 of 1346 strings)

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

* Translated using Weblate (Czech)

Currently translated at 93.8% (1407 of 1500 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (1503 of 1503 strings)

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (1503 of 1503 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (1503 of 1503 strings)

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

* Translated using Weblate (German)

Currently translated at 100.0% (1504 of 1504 strings)

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

* Translated using Weblate (Czech)

Currently translated at 93.8% (1411 of 1504 strings)

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

* Translated using Weblate (Czech)

Currently translated at 93.8% (1411 of 1504 strings)

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

* Translated using Weblate (Czech)

Currently translated at 93.8% (1411 of 1504 strings)

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

* Translated using Weblate (Czech)

Currently translated at 93.8% (1411 of 1504 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (1504 of 1504 strings)

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (1504 of 1504 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (1504 of 1504 strings)

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

* Translated using Weblate (Czech)

Currently translated at 93.8% (1412 of 1504 strings)

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

* Translated using Weblate (Czech)

Currently translated at 93.8% (1412 of 1504 strings)

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

* Translated using Weblate (Turkish)

Currently translated at 81.7% (1230 of 1504 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 100.0% (1504 of 1504 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 100.0% (1346 of 1346 strings)

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

* Translated using Weblate (Turkish)

Currently translated at 83.2% (1252 of 1504 strings)

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

* Update translation files

Updated by "Cleanup translation files" hook in Weblate.

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (1507 of 1507 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 100.0% (1507 of 1507 strings)

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

* Translated using Weblate (German)

Currently translated at 100.0% (1507 of 1507 strings)

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (1508 of 1508 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (1508 of 1508 strings)

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

* Translated using Weblate (German)

Currently translated at 100.0% (1508 of 1508 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 100.0% (1508 of 1508 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 100.0% (1346 of 1346 strings)

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

* Translated using Weblate (Hungarian)

Currently translated at 42.4% (640 of 1508 strings)

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

* Translated using Weblate (Hungarian)

Currently translated at 55.0% (830 of 1508 strings)

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

* Translated using Weblate (Russian)

Currently translated at 99.8% (1506 of 1508 strings)

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

* Translated using Weblate (Arabic)

Currently translated at 99.7% (1504 of 1508 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (1508 of 1508 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 99.9% (1507 of 1508 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 98.5% (1326 of 1346 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 100.0% (1508 of 1508 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 100.0% (1346 of 1346 strings)

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

* Translated using Weblate (Hungarian)

Currently translated at 80.4% (1213 of 1508 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 100.0% (1508 of 1508 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 100.0% (1346 of 1346 strings)

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

* Translated using Weblate (Hungarian)

Currently translated at 80.6% (1216 of 1508 strings)

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

* Translated using Weblate (Bulgarian)

Currently translated at 100.0% (1508 of 1508 strings)

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

* Translated using Weblate (Hungarian)

Currently translated at 82.4% (1244 of 1508 strings)

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

* Translated using Weblate (Hungarian)

Currently translated at 86.4% (1304 of 1508 strings)

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

* Update translation files

Updated by "Remove blank strings" hook in Weblate.

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (1508 of 1508 strings)

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

* Translated using Weblate (Russian)

Currently translated at 99.9% (1507 of 1508 strings)

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

* Translated using Weblate (Greek)

Currently translated at 6.2% (94 of 1508 strings)

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

* Translated using Weblate (Hungarian)

Currently translated at 97.6% (1473 of 1508 strings)

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

* Translated using Weblate (Hungarian)

Currently translated at 97.6% (1473 of 1508 strings)

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

* Update translation files

Updated by "Remove blank strings" hook in Weblate.

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

* Translated using Weblate (Greek)

Currently translated at 9.3% (141 of 1508 strings)

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

* Update translation files

Updated by "Remove blank strings" hook in Weblate.

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

* Translated using Weblate (Greek)

Currently translated at 13.3% (201 of 1508 strings)

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

* Translated using Weblate (Hungarian)

Currently translated at 100.0% (1508 of 1508 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (1510 of 1510 strings)

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

* Translated using Weblate (Arabic)

Currently translated at 99.7% (1506 of 1510 strings)

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

* Translated using Weblate (Greek)

Currently translated at 14.2% (215 of 1510 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (1510 of 1510 strings)

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

* Translated using Weblate (Hungarian)

Currently translated at 100.0% (1510 of 1510 strings)

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

* Translated using Weblate (Greek)

Currently translated at 14.8% (224 of 1510 strings)

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

* Translated using Weblate (Greek)

Currently translated at 1.1% (16 of 1346 strings)

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (1510 of 1510 strings)

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (1346 of 1346 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 100.0% (1510 of 1510 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 100.0% (1510 of 1510 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (1510 of 1510 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (1346 of 1346 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 100.0% (1510 of 1510 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 100.0% (1346 of 1346 strings)

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

* Translated using Weblate (Hungarian)

Currently translated at 100.0% (1510 of 1510 strings)

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

* Translated using Weblate (German)

Currently translated at 100.0% (1510 of 1510 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 100.0% (1346 of 1346 strings)

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

* fix kotlin strings, import/export ios

---------

Co-authored-by: Random <random-r@users.noreply.hosted.weblate.org>
Co-authored-by: Corey Lin <lyfone@gmail.com>
Co-authored-by: xe1st <dnzkckali@gmail.com>
Co-authored-by: Istvan Novak <easthvan@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Maksym Lukashenko <livelmaxim@gmail.com>
Co-authored-by: jonnysemon <jonnysemon@users.noreply.hosted.weblate.org>
Co-authored-by: 小林照幸 <koba1014@gmail.com>
Co-authored-by: inson1 <vaclav.svarc01@seznam.cz>
Co-authored-by: Ophiushi <41908476+ishi-sama@users.noreply.github.com>
Co-authored-by: Eric <zxmegaxqug@hldrive.com>
Co-authored-by: mlanp <github@lang.xyz>
Co-authored-by: zenobit <zen@osowoso.xyz>
Co-authored-by: M1K4 <oomikaoo@gmail.com>
Co-authored-by: No name <CertainBot@users.noreply.hosted.weblate.org>
Co-authored-by: summoner001 <summoner@vivaldi.net>
Co-authored-by: v1s7 <v1s7@users.noreply.hosted.weblate.org>
Co-authored-by: elgratea <weblate@fastmail.com>
Co-authored-by: diodepon <diopon@mailo.com>
2023-12-28 17:31:40 +00:00
Stanislav Dmitrenko
5b52d0e173 android, desktop: localization script enchancement (#3616) 2023-12-28 15:53:37 +00:00
Evgeny Poberezkin
b4dd6941d7 core: allow quoted strings (with spaces) in network interface of remote hosts (#3592) 2023-12-28 10:06:49 +00:00
Stanislav Dmitrenko
07334b057d desktop (gradle): support specifying more versions in Gradle (#3614) 2023-12-28 09:41:17 +00:00
Evgeny Poberezkin
51bf2f413c 5.4.2: ios 186, android 166, desktop 20 2023-12-27 22:24:21 +00:00
Evgeny Poberezkin
e3a69b12ba core: 5.4.2.1 (simplexmq 5.5.0.2) 2023-12-27 21:00:19 +00:00
Evgeny Poberezkin
b220b5f6ec core: fix contact subscriptions (#3611) 2023-12-27 18:55:50 +00:00
Stanislav Dmitrenko
0b8346701c android, desktop: fix terminal items crash (#3607)
* android, desktop: fix terminal items crash

* test

* Revert "test"

This reverts commit 3f1e5c93ca.

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-12-27 14:59:38 +00:00
Evgeny Poberezkin
efc873b09b core: min version for remote connection 5.4.2.0 (#3594) 2023-12-27 14:48:28 +00:00
Stanislav Dmitrenko
9f71502b51 desktop (windows): fix script that generates localization (#3606)
* desktop (windows): fix script that generates localization

* firstOrNull
2023-12-27 14:47:04 +00:00
Evgeny Poberezkin
bbde6d81ee core: update simplexmq 2023-12-27 13:57:02 +00:00
Stanislav Dmitrenko
58906e1a60 desktop (windows): fixed handling non-utf8 Windows profile names (#3605) 2023-12-27 13:56:48 +00:00
Stanislav Dmitrenko
ed3d234826 android, desktop: limit text length in terminal view (#3604) 2023-12-27 11:27:34 +00:00
Stanislav Dmitrenko
dded56d8b8 ios: converting video to mp4 and making quality lower (#3597)
* ios: converting video to mp4 and making quality lower

Lower quality allows to play that videos on Android as well

* update export settings

---------

Co-authored-by: Avently <avently@local>
Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-12-27 11:23:46 +00:00
spaced4ndy
4d5aefa82c ui: don't share address at onboarding by default (#3603)
* ios: don't share address at onboarding by default

* android
2023-12-27 10:54:44 +00:00
Evgeny Poberezkin
9ac99ec2d9 core: update simplexmq (mark failed work items to continue processing) (#3600)
* core: update simplexmq (mark failed work items to continue processing) WIP

* simplexmq
2023-12-26 19:53:58 +00:00
spaced4ndy
37d033c7a5 core: test group members connect in group when they were previously connected as contacts (#3595) 2023-12-25 11:03:02 +04:00
spaced4ndy
5798efcf71 code: modify test 2023-12-24 20:55:03 +04:00
spaced4ndy
e086719f27 core: add to tests 2023-12-24 20:44:30 +04:00
spaced4ndy
bb4293eb5e fix tests 2023-12-24 20:23:09 +04:00
Evgeny Poberezkin
c3c66182f2 Merge branch 'stable' 2023-12-24 14:20:58 +00:00
Evgeny Poberezkin
5b90d92ca2 core: add group tests 2023-12-24 14:20:07 +00:00
Evgeny Poberezkin
af22348bf8 core: use version from config, add tests (#3588)
* core: use version from config, add tests

* comment

* refactor
2023-12-24 13:27:51 +00:00
Stanislav Dmitrenko
5a6670998c android, desktop: loading prev messages better (#3585)
* android, desktop: loading prev messages better

* better

* better

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-12-24 12:22:00 +00:00
spaced4ndy
700f6fa663 core, docs: drop message views if they exist, remove mentions in docs (#3589)
* core, docs: drop message views if they exist, remove mentions in docs

* fix migration
2023-12-24 11:13:34 +00:00
Stanislav Dmitrenko
d474cae705 ios: saving and sharing items menu item (#3581)
* ios: saving and sharing items menu item

* refactor

---------

Co-authored-by: Avently <avently@local>
Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-12-23 23:15:31 +00:00
Evgeny Poberezkin
2834b192ce core: do not create group history item (#3586)
* core: do not create group history item

* fix tests
2023-12-23 19:56:01 +00:00
Evgeny Poberezkin
b8cb954882 ios: update library 2023-12-23 17:24:30 +00:00
Evgeny Poberezkin
6aeef6f132 5.4.2.0: fix migration in simplexmq 2023-12-23 16:09:08 +00:00
Evgeny Poberezkin
fa1702a566 5.4.2.0: update .cabal 2023-12-23 14:13:38 +00:00
Evgeny Poberezkin
95d6df926c 5.4.2.0 2023-12-23 13:46:11 +00:00
Evgeny Poberezkin
cccd517277 core: fix simplexmq commit 2023-12-23 13:08:40 +00:00
spaced4ndy
12d1ada25e core: support batch sending in groups, batch introductions; send recent message history to new members (#3519)
* core: batch send stubs, comments

* multiple events in ChatMessage and supporting types

* Revert "multiple events in ChatMessage and supporting types"

This reverts commit 9b239b26ba.

* schema, refactor group processing for batched messages

* encoding, refactor processing

* refactor code to work with updated schema

* encoding, remove instances

* wip

* implement batching

* batch introductions

* wip

* collect and send message history

* missing new line

* rename

* test

* rework to build history via chat items

* refactor, tests

* correctly set member version range, dont include deleted items

* tests

* fix disappearing messages

* check number of errors

* comment

* check size in encodeChatMessage

* fix - don't check msg size for binary

* use builder

* rename

* rename

* rework batching

* lazy msg body

* use withStoreBatch

* refactor

* reverse batches

* comment

* possibly fix builder for single msg

* refactor batcher

* refactor

* dont repopulate msg_deliveries on down migration

* EncodedChatMessage type

* remove type

* batcher tests

* add tests

* group history preference

* test group link

* fix tests

* fix for random update

* add test testImageFitsSingleBatch

* refactor

* rename function

* refactor

* mconcat

* rename feature

* catch error on each batch

* refactor file inv retrieval

* refactor gathering item forward events

* refactor message batching

* unite migrations

* move files

* refactor

* Revert "unite migrations"

This reverts commit 0be7a3117a.

* refactor splitFileDescr

* improve tests

* Revert "dont repopulate msg_deliveries on down migration"

This reverts commit 2944c1cc28.

* fix down migration

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-12-23 13:07:23 +00:00
Evgeny Poberezkin
f93f68e425 core: agent background mode for iOS NSE (#3574)
* core: agent background mode for iOS NSE

* change parameter for APIActivateChat

* fix

* update lib

* update lib

* simplexmq

* simplify
2023-12-23 13:06:59 +00:00
Andor Kesselman
23989aca57 Update README.md (#3553) 2023-12-22 08:48:26 +00:00
Andor Kesselman
57a6e85668 docs: fix typo (#3552) 2023-12-22 08:47:48 +00:00
Stanislav Dmitrenko
67590f3258 Revert "ios: making thumbnails faster" (#3571)
This reverts commit cd9cb8e064.
2023-12-22 08:46:55 +00:00
Stanislav Dmitrenko
c83238c35a desktop: enable sending images and files with enter (#3582) 2023-12-21 22:48:32 +00:00
Stanislav Dmitrenko
8b0d2dede7 android, desktop: saving and sharing files menu item (#3580) 2023-12-21 15:19:36 +00:00
Stanislav Dmitrenko
c4855313b6 android: splash screen with background color on Android 12+ (#3579) 2023-12-21 13:49:49 +00:00
Evgeny Poberezkin
2bff3b9c97 desktop, android: update api to pass controller when encrypting files (use ChaChaDRG as source of randomness) (#3578) 2023-12-21 12:49:18 +00:00
Evgeny Poberezkin
aa037c0662 ios: update core library (uses GHC 9.6.3) 2023-12-21 10:05:43 +00:00
Evgeny Poberezkin
d198d6a8db core: build iOS library with ghc 9.6.3 with iPhone7 etc. support (#3577)
* bump haskell.nix

* bump flake.lock

* Try openssl fix

* CFLAGS. not CCFLAGS

* Fix iOS build issues and improve static library handling

---------

Co-authored-by: Moritz Angermann <moritz.angermann@gmail.com>
2023-12-21 09:57:43 +00:00
Evgeny Poberezkin
7bcda7e54b core: use ChaChaDRG as the source of randomness (#3551)
* core: use ChaChaDRG as the source of randomness

* do not use entropy directly

* dont use RNG from agent

* simplexmq

* update iOS
2023-12-21 00:42:40 +00:00
Stanislav Dmitrenko
4a4d470859 android, desktop: try-catch composables (#3575)
* android, desktop: try-catch composables

* test

* better catching on Android

* more try-catch'es

* Revert "test"

This reverts commit adaf92b116.

* more try-catch'es

* unneeded imports
2023-12-20 18:00:44 +00:00
Evgeny Poberezkin
6ba3100d34 core: batch sending messages (#3566)
* core: batch sending messages

* batch without iorefs (#3573)

* one-pass

* simplexmq

* simplexmq

* simplexmq

* simplexmq

* revert change to ios project file

* refactor

* simplify

---------

Co-authored-by: Alexander Bondarenko <486682+dpwiz@users.noreply.github.com>
2023-12-20 10:38:39 +04:00
Evgeny Poberezkin
7b073ba9f8 core: allow deleting last user (#3567)
* core: allow deleting last user (tests fail)

* tests, allow activating the hidden user when there is no active user

* hide logs

Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>

* comment

Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>

* comment

Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>

---------

Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
2023-12-19 10:26:01 +00:00
Stanislav Dmitrenko
5e042d222e desktop: saving qr code as an image (#3572) 2023-12-19 10:24:13 +00:00
Stanislav Dmitrenko
26a189917b sctipt: check string formatting (#3570)
* sctipt: check string formatting

* all
2023-12-18 21:37:10 +00:00
spaced4ndy
ce9218b186 ios: rework authentication (#3556) 2023-12-18 22:04:49 +04:00
Evgeny Poberezkin
f0338a03d1 directory: better search, allow both simplex:/ and simplex.chat links in description (#3546)
* directory: new commands

* better search

* search test

* return group links in simplex.chat domain, allow both simplex:/ and simplex.chat links in group description
2023-12-18 10:41:08 +00:00
Evgeny Poberezkin
6fa0001ea7 ios: delay suspendChat in NSE, background schedule depends on notifications mode (#3561)
* ios: delay suspendChat in NSE

* different background refresh interval depending on the settings

* simplify

* comment

* reduce NSE suspend interval

* space
2023-12-18 10:36:25 +00:00
Stanislav Dmitrenko
974fa448b4 android, desktop: some alerts became privacy sensitive (#3554)
* android, desktop: some alerts became privacy sensitive

* changes
2023-12-14 13:11:19 +00:00
spaced4ndy
8cec5428ee core: save CIContent tag in chat_items table (#3555) 2023-12-14 17:08:40 +04:00
Evgeny Poberezkin
73130bf321 ios: update core library 2023-12-13 21:48:25 +00:00
spaced4ndy
67241ff65c ios: fix code scanners only attempting to scan once (#3548) 2023-12-13 16:13:05 +04:00
spaced4ndy
b6b041490f core: improve chat list pagination performance, simplify logic by always reading chat stats and last item id for previews (#3541)
* core: improve chat list pagination performance

* fix query

* core: improve chat list pagination performance, simplify logic by always reading chat stats (#3543)

* microseconds

* fix

* update simplexmq

* simplify queries

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-12-13 15:32:23 +04:00
Evgeny Poberezkin
7f9f9a674c ios: fix member view freezing on iOS 15, closes #3487 (#3547) 2023-12-13 11:27:28 +00:00
Evgeny Poberezkin
ae94bb6f87 core: use crypton instead of cryptonite (#3542)
* update hackage

* use crypton instead of cryptonite

* remove http2 from cabal.project

* simplexmq
2023-12-13 11:20:03 +00:00
Evgeny Poberezkin
7ec39d1ffa all: increase default TCP timeouts, update simplexmq (#3540) 2023-12-12 13:13:36 +00:00
Evgeny Poberezkin
ca6dfb5ea1 docs: update latest version 2023-12-12 10:25:02 +00:00
Evgeny Poberezkin
a5048db6fa ios: improve media picker for multiple images/videos (#3538)
* ios: improve media picker to work with multiple images reliably

* MainActor
2023-12-12 09:04:48 +00:00
Evgeny Poberezkin
aca3a71b38 ios: update library 2023-12-11 18:57:42 +00:00
Evgeny Poberezkin
d358390e3d Merge pull request #3532 from simplex-chat/ios-notifications
improve iOS notifications
2023-12-11 14:55:03 +00:00
Evgeny Poberezkin
f9a125bc32 Merge branch 'master' into ios-notifications 2023-12-11 14:11:00 +00:00
Alexander Bondarenko
35c1975d66 core: chat list pagination (#3505)
* add pagination args to APIGetChats

* add search to chat list API

* rename arg to paginationTs_ to match type

* lift another condition to ids query

* collect all chat refs before sorting, then get details

* split remaining preview functions

* roll back to collecting ids first with query cleanup

* add connection join back to filter out groups

* extract and expand tests

* add fav/unread args

* WIP

* lay out the queries with favs

* tweak tests

* add fav tests

* fix order by in the before case

* build query footer wholly from pagination

* add migration for direct contacts

* fix setting contact_used

* fix setting contact_used for group link contacts

* align search x filters space with UI, support filter by either favorite or unread, optimize queries, indexes

* always set chat_ts, fix tests

* refactor tests

* fix pagination logic, more tests

* refactor, rename

* increase default pagination count

* comments

* refactor

* comment

* report errors

* refactor

* remove unused type

---------

Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
2023-12-11 17:50:32 +04:00
Evgeny Poberezkin
0bfe37137c core: update simplexmq (message notification markers) 2023-12-11 13:11:35 +00:00
Evgeny Poberezkin
666903ae76 Merge branch 'master' into ios-notifications 2023-12-11 13:06:32 +00:00
Evgeny Poberezkin
8a41a4c214 ios: do not start chat if it was stopped, deliver "app stopped" notifications (#3535)
* add stopped notifications, remove full off mode

* core: allow initializing chat data without starting chat

* ios: ask before starting chat if it was stopped

* correct text

Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>

* fix comment

---------

Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
2023-12-11 12:59:49 +00:00
Evgeny Poberezkin
79a954336c ios: communication between NSE and app via files (#3533)
* ios: communication between NSE and app via files

* clean up

* better concurrency
2023-12-11 12:34:56 +00:00
Evgeny Poberezkin
f65b8a9e78 core: mark all user messages read (#3530) 2023-12-11 12:26:45 +00:00
Evgeny Poberezkin
e8016adfdc simplexmq 2023-12-10 17:47:44 +00:00
Evgeny Poberezkin
d3059afc99 ios, core: better notifications processing to avoid contention for database (#3485)
* core: forward notifications about message processing (for iOS notifications)

* simplexmq

* the option to keep database key, to allow re-opening the database

* export new init with keepKey and reopen DB api

* stop remote ctrl when suspending chat

* ios: close/re-open db on suspend/activate

* allow activating chat without restoring (for NSE)

* update NSE to suspend/activate (does not work)

* simplexmq

* suspend chat and close database when last notification in the process is processed

* stop reading notifications on message markers

* replace async stream with cancellable concurrent queue

* better synchronization of app and NSE

* remove outside of task

* remove unused var

* whitespace

* more debug logging, handle cancelled read after dequeue

* comments

* more comments
2023-12-09 21:59:40 +00:00
Evgeny Poberezkin
2f7632a70f 5.4.1: ios 185, android 164, desktop 19 2023-12-07 21:01:14 +00:00
Evgeny Poberezkin
27c14f32f1 core: 5.4.0.7 2023-12-07 14:30:42 +00:00
Stanislav Dmitrenko
13a32f7864 android: made minimum supported version of Android as 9 (#3525) 2023-12-07 10:49:16 +00:00
Stanislav Dmitrenko
b1652b8930 desktop: fix toasts theme (#3524) 2023-12-06 21:19:30 +00:00
Stanislav Dmitrenko
a9b36e8e39 desktop: fix onboarding when choosing random password (#3523) 2023-12-06 20:33:53 +00:00
Evgeny Poberezkin
ee163a6540 core: add missing field 2023-12-06 12:34:10 +00:00
Evgeny Poberezkin
4fd6405113 core: update simplexmq (better suspend agent) 2023-12-06 00:19:24 +00:00
Stanislav Dmitrenko
ccc62274ee android, desktop: crash handler addition (#3517)
* android, desktop: crash handler addition

* added
2023-12-05 22:50:25 +00:00
Stanislav Dmitrenko
4c6d52ba75 android, desktop: crash handler (#3516)
* android, desktop: crash handler

* test

* rename

* string

* Revert "test"

This reverts commit 530faf39c1.

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-12-05 21:31:49 +00:00
Evgeny Poberezkin
9df63160e5 ios: fix simplex address view (#3515)
* ios: fix simplex address view

* fix lib paths

* fix call
2023-12-05 09:48:04 +00:00
Stanislav Dmitrenko
c8e9788c29 desktop: enhancements to remote desktop connect UI (#3513)
* desktop: enhancements to remote desktop connect UI

* changes

* more changes

This reverts commit e8323e8bfa.

* color

* random port
2023-12-04 21:04:58 +00:00
spaced4ndy
7099776357 docs: fix typo 2023-12-04 19:17:24 +04:00
Evgeny Poberezkin
3481d379c6 core: compatibility with GHC 8.10.7, narrow dependency ranges (#3503)
* Revert "raise lower bound on mtl to a real version (#3499)"

This reverts commit f94c0311c1.

* Revert "core: expand ranges to fit ghc 8.10 & 9.6 (#3496)"

This reverts commit 9a1c7f41f7.

* update simplexmq

* remove netword-transport fork

* simplexmq

* fix test

* fix index-state in cabal.project

* simplexmq

* simplexmq

* bytestring,simplexmq

* template-haskell, simplexmq

* simplexmq

* simplexmq

* mtl

* simplexmq
2023-12-04 10:01:37 +00:00
Evgeny Poberezkin
85c1e871dc Merge branch 'stable' 2023-12-04 00:06:53 +00:00
Evgeny Poberezkin
fec5ff3f15 docs: SimpleX address (#3508)
* docs: SimpleX address

* table

* header
2023-12-03 22:21:13 +00:00
Evgeny Poberezkin
acaa597c90 desktop, android: fix image not appearing in view when received (#3504)
* desktop, android: fix image not appearing in view when received

* change to KeyChangeEffect
2023-12-03 15:42:43 +00:00
Evgeny Poberezkin
6a9a67db14 cli: option to mark shown messages as read (off by default) (#3506)
* cli: option to mark shown messages as read (off by default)

* fix tests

* fix tests
2023-12-03 15:42:26 +00:00
Alexander Bondarenko
f94c0311c1 raise lower bound on mtl to a real version (#3499)
* raise lower bound on mtl to a real version

* simplexmq

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-12-02 12:24:29 +00:00
Stanislav Dmitrenko
e1ff7c88d7 desktop: allow changing listening ip and port of remote (#3498)
* desktop: allow changing listening ip and port of remote

* remove empty lines

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-12-01 20:41:08 +00:00
Alexander Bondarenko
9a1c7f41f7 core: expand ranges to fit ghc 8.10 & 9.6 (#3496)
* expand ranges to fit ghc 8.10 & 9.6

* update nix

* use hashes from mq master

* fix more deps

* use network-transport from hackage
2023-12-01 16:52:47 +00:00
Stanislav Dmitrenko
40e69ae713 desktop: enable database operations (#3495)
* desktop: enable database operations

* disconnect hosts button

* not relaying on dev tools

* different logic

* different logic 2

* toggle placement
2023-12-01 15:04:00 +00:00
spaced4ndy
b74e33b958 docs: inactive group members rfc (#3419) 2023-12-01 19:02:50 +04:00
Stanislav Dmitrenko
540c8883a0 android: do not show alert too early in obboarding (#3493) 2023-11-30 19:39:16 +00:00
Stanislav Dmitrenko
0e18b13bea desktop: adapting onboarding process to linking devices (#3490)
* desktop: adapting onboarding process to linking devices

* show progress on long operations

* changes

* clearing chat cache logic

* lines

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-11-30 19:38:21 +00:00
spaced4ndy
a4b44254bc core: update simplexmq (ghc 8.10.7 compatibility) (#3492) 2023-11-30 21:09:07 +04:00
spaced4ndy
5819e42305 core: remove CRNewContactConnection response; mobile, desktop: create pending connections based on api responses (CRNewContactConnection was being used as "event" in UI) (#3489) 2023-11-30 20:31:32 +04:00
Jesse Horne
9580b4110d desktop: remember window position and size (#3465)
* initial work on storing desktop window position and size

* removed useless imports

* updated to use app preferences

* vars to vals

* defensive programming

* fixed default

* removed default json

* do nothing if encoding to json while storing fails

* names, clean up

* move comment

* changes

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
Co-authored-by: Avently <7953703+avently@users.noreply.github.com>
2023-11-30 12:43:01 +00:00
Stanislav Dmitrenko
05a64c99a2 ios: moving webrtc commands processing to another mechanism (#3480)
* ios: moving webrtc commands processing to another mechanism

* async

* decide

* handle errors

* error alert

* await

---------

Co-authored-by: Avently <avently@local>
Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-11-28 17:36:05 +00:00
Alexander Bondarenko
6a21d5c7f1 add remote host bindings (#3471)
* add remote host bindings

* group iface/address together

* rename migration

* add implementation

* update view and api

* bump upstream

* add schema

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-11-28 16:32:33 +00:00
Stanislav Dmitrenko
950bbe19da ios: fix calls connecting state (#3475)
* ios: fix calls connecting state

* optimization

* changes

* removed relay protocol

* simplify

* use actor

* fix loop, better onChange, some questions

* remove extra iteration

---------

Co-authored-by: Avently <avently@local>
Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-11-27 22:20:51 +00:00
Stanislav Dmitrenko
f31054de4f desktop (windows): fix action (#3479) 2023-11-27 22:11:53 +00:00
Evgeny Poberezkin
05278e5a06 core: allow remote host commands without user (#3478) 2023-11-27 18:34:15 +00:00
spaced4ndy
7a54d74517 Revert "ios: update libraries (#3474)"
This reverts commit bfcb2ac230.
2023-11-27 19:16:53 +04:00
spaced4ndy
bfcb2ac230 ios: update libraries (#3474) 2023-11-27 19:02:44 +04:00
spaced4ndy
3073c4a1d5 core: fix chat previews showing not the latest message, fix message ordering in direct chats; mobile: update group previews only on timestamp increase (#3473) 2023-11-27 17:14:12 +04:00
Evgeny Poberezkin
d4ac1c0cf2 core, ui: add remote host/controller stop reasons to events (#3472) 2023-11-26 23:23:37 +00:00
Evgeny Poberezkin
d29f1bb0cf core: use fourmolu styles (#3470) 2023-11-26 18:16:37 +00:00
Jesse Horne
75c2de8a12 desktop: closing console window no longer closes entire application (#3466)
* closing the console now doesn't close all windows

* simplify

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-11-26 13:40:51 +00:00
Alexander Bondarenko
f20ac33e67 cli: remove clashing short option for device-name (#3468) 2023-11-26 13:16:32 +00:00
Evgeny Poberezkin
8cc0954430 Merge branch 'stable' 2023-11-26 11:50:30 +00:00
qvsojBJGiEnR
8f6a31ca07 Update app-settings.md (#3379) 2023-11-17 23:29:25 +00:00
480 changed files with 39721 additions and 13822 deletions

View File

@@ -9,6 +9,7 @@ on:
tags:
- "v*"
- "!*-fdroid"
- "!*-armv7a"
pull_request:
jobs:
@@ -42,7 +43,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
build:
name: build-${{ matrix.os }}
name: build-${{ matrix.os }}-${{ matrix.ghc }}
if: always()
needs: prepare-release
runs-on: ${{ matrix.os }}
@@ -51,18 +52,25 @@ jobs:
matrix:
include:
- os: ubuntu-20.04
ghc: "8.10.7"
cache_path: ~/.cabal/store
- os: ubuntu-20.04
ghc: "9.6.3"
cache_path: ~/.cabal/store
asset_name: simplex-chat-ubuntu-20_04-x86-64
desktop_asset_name: simplex-desktop-ubuntu-20_04-x86_64.deb
- os: ubuntu-22.04
ghc: "9.6.3"
cache_path: ~/.cabal/store
asset_name: simplex-chat-ubuntu-22_04-x86-64
desktop_asset_name: simplex-desktop-ubuntu-22_04-x86_64.deb
- os: macos-latest
ghc: "9.6.3"
cache_path: ~/.cabal/store
asset_name: simplex-chat-macos-x86-64
desktop_asset_name: simplex-desktop-macos-x86_64.dmg
- os: windows-latest
ghc: "9.6.3"
cache_path: C:/cabal
asset_name: simplex-chat-windows-x86-64
desktop_asset_name: simplex-desktop-windows-x86_64.msi
@@ -81,16 +89,17 @@ jobs:
- name: Setup Haskell
uses: haskell-actions/setup@v2
with:
ghc-version: "9.6.3"
ghc-version: ${{ matrix.ghc }}
cabal-version: "3.10.1.0"
- name: Cache dependencies
uses: actions/cache@v3
- name: Restore cached build
id: restore_cache
uses: actions/cache/restore@v3
with:
path: |
${{ matrix.cache_path }}
dist-newstyle
key: ${{ matrix.os }}-${{ hashFiles('cabal.project', 'simplex-chat.cabal') }}
key: ${{ matrix.os }}-ghc${{ matrix.ghc }}-${{ hashFiles('cabal.project', 'simplex-chat.cabal') }}
# / Unix
@@ -105,7 +114,7 @@ jobs:
echo " flags: +openssl" >> cabal.project.local
- name: Install AppImage dependencies
if: startsWith(github.ref, 'refs/tags/v') && matrix.os == 'ubuntu-20.04'
if: startsWith(github.ref, 'refs/tags/v') && matrix.asset_name && matrix.os == 'ubuntu-20.04'
run: sudo apt install -y desktop-file-utils
- name: Install pkg-config for Mac
@@ -131,7 +140,7 @@ jobs:
echo "bin_hash=$(echo SHA2-512\(${{ matrix.asset_name }}\)= $(openssl sha512 $path | cut -d' ' -f 2))" >> $GITHUB_OUTPUT
- name: Unix upload CLI binary to release
if: startsWith(github.ref, 'refs/tags/v') && matrix.os != 'windows-latest'
if: startsWith(github.ref, 'refs/tags/v') && matrix.asset_name && matrix.os != 'windows-latest'
uses: svenstaro/upload-release-action@v2
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
@@ -140,7 +149,7 @@ jobs:
tag: ${{ github.ref }}
- name: Unix update CLI binary hash
if: startsWith(github.ref, 'refs/tags/v') && matrix.os != 'windows-latest'
if: startsWith(github.ref, 'refs/tags/v') && matrix.asset_name && matrix.os != 'windows-latest'
uses: softprops/action-gh-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -150,7 +159,7 @@ jobs:
${{ steps.unix_cli_build.outputs.bin_hash }}
- name: Setup Java
if: startsWith(github.ref, 'refs/tags/v')
if: startsWith(github.ref, 'refs/tags/v') && matrix.asset_name
uses: actions/setup-java@v3
with:
distribution: 'corretto'
@@ -159,7 +168,7 @@ jobs:
- name: Linux build desktop
id: linux_desktop_build
if: startsWith(github.ref, 'refs/tags/v') && (matrix.os == 'ubuntu-20.04' || matrix.os == 'ubuntu-22.04')
if: startsWith(github.ref, 'refs/tags/v') && matrix.asset_name && (matrix.os == 'ubuntu-20.04' || matrix.os == 'ubuntu-22.04')
shell: bash
run: |
scripts/desktop/build-lib-linux.sh
@@ -168,10 +177,10 @@ jobs:
path=$(echo $PWD/release/main/deb/simplex_*_amd64.deb)
echo "package_path=$path" >> $GITHUB_OUTPUT
echo "package_hash=$(echo SHA2-512\(${{ matrix.desktop_asset_name }}\)= $(openssl sha512 $path | cut -d' ' -f 2))" >> $GITHUB_OUTPUT
- name: Linux make AppImage
id: linux_appimage_build
if: startsWith(github.ref, 'refs/tags/v') && matrix.os == 'ubuntu-20.04'
if: startsWith(github.ref, 'refs/tags/v') && matrix.asset_name && matrix.os == 'ubuntu-20.04'
shell: bash
run: |
scripts/desktop/make-appimage-linux.sh
@@ -194,7 +203,7 @@ jobs:
echo "package_hash=$(echo SHA2-512\(${{ matrix.desktop_asset_name }}\)= $(openssl sha512 $path | cut -d' ' -f 2))" >> $GITHUB_OUTPUT
- name: Linux upload desktop package to release
if: startsWith(github.ref, 'refs/tags/v') && (matrix.os == 'ubuntu-20.04' || matrix.os == 'ubuntu-22.04')
if: startsWith(github.ref, 'refs/tags/v') && matrix.asset_name && (matrix.os == 'ubuntu-20.04' || matrix.os == 'ubuntu-22.04')
uses: svenstaro/upload-release-action@v2
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
@@ -203,7 +212,7 @@ jobs:
tag: ${{ github.ref }}
- name: Linux update desktop package hash
if: startsWith(github.ref, 'refs/tags/v') && (matrix.os == 'ubuntu-20.04' || matrix.os == 'ubuntu-22.04')
if: startsWith(github.ref, 'refs/tags/v') && matrix.asset_name && (matrix.os == 'ubuntu-20.04' || matrix.os == 'ubuntu-22.04')
uses: softprops/action-gh-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -213,7 +222,7 @@ jobs:
${{ steps.linux_desktop_build.outputs.package_hash }}
- name: Linux upload AppImage to release
if: startsWith(github.ref, 'refs/tags/v') && matrix.os == 'ubuntu-20.04'
if: startsWith(github.ref, 'refs/tags/v') && matrix.asset_name && matrix.os == 'ubuntu-20.04'
uses: svenstaro/upload-release-action@v2
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
@@ -222,7 +231,7 @@ jobs:
tag: ${{ github.ref }}
- name: Linux update AppImage hash
if: startsWith(github.ref, 'refs/tags/v') && matrix.os == 'ubuntu-20.04'
if: startsWith(github.ref, 'refs/tags/v') && matrix.asset_name && matrix.os == 'ubuntu-20.04'
uses: softprops/action-gh-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -250,6 +259,15 @@ jobs:
body: |
${{ steps.mac_desktop_build.outputs.package_hash }}
- name: Cache unix build
uses: actions/cache/save@v3
if: matrix.os != 'windows-latest'
with:
path: |
${{ matrix.cache_path }}
dist-newstyle
key: ${{ steps.restore_cache.outputs.cache-primary-key }}
- name: Unix test
if: matrix.os != 'windows-latest'
timeout-minutes: 30
@@ -262,7 +280,7 @@ jobs:
# rm -rf dist-newstyle/src/direct-sq* is here because of the bug in cabal's dependency which prevents second build from finishing
- name: 'Setup MSYS2'
if: startsWith(github.ref, 'refs/tags/v') && matrix.os == 'windows-latest'
if: matrix.os == 'windows-latest'
uses: msys2/setup-msys2@v2
with:
msystem: ucrt64
@@ -281,7 +299,7 @@ jobs:
if: matrix.os == 'windows-latest'
shell: msys2 {0}
run: |
export PATH=$PATH:/c/ghcup/bin
export PATH=$PATH:/c/ghcup/bin:$(echo /c/tools/ghc-*/bin || echo)
scripts/desktop/prepare-openssl-windows.sh
openssl_windows_style_path=$(echo `pwd`/dist-newstyle/openssl-1.1.1w | sed 's#/\([a-zA-Z]\)#\1:#' | sed 's#/#\\#g')
rm cabal.project.local 2>/dev/null || true
@@ -323,14 +341,14 @@ jobs:
if: startsWith(github.ref, 'refs/tags/v') && matrix.os == 'windows-latest'
shell: msys2 {0}
run: |
export PATH=$PATH:/c/ghcup/bin
export PATH=$PATH:/c/ghcup/bin:$(echo /c/tools/ghc-*/bin || echo)
scripts/desktop/build-lib-windows.sh
cd apps/multiplatform
./gradlew packageMsi
path=$(echo $PWD/release/main/msi/*imple*.msi | sed 's#/\([a-z]\)#\1:#' | sed 's#/#\\#g')
echo "package_path=$path" >> $GITHUB_OUTPUT
echo "package_hash=$(echo SHA2-512\(${{ matrix.desktop_asset_name }}\)= $(openssl sha512 $path | cut -d' ' -f 2))" >> $GITHUB_OUTPUT
- name: Windows upload desktop package to release
if: startsWith(github.ref, 'refs/tags/v') && matrix.os == 'windows-latest'
uses: svenstaro/upload-release-action@v2
@@ -350,4 +368,13 @@ jobs:
body: |
${{ steps.windows_desktop_build.outputs.package_hash }}
- name: Cache windows build
uses: actions/cache/save@v3
if: matrix.os == 'windows-latest'
with:
path: |
${{ matrix.cache_path }}
dist-newstyle
key: ${{ steps.restore_cache.outputs.cache-primary-key }}
# Windows /

View File

@@ -72,7 +72,7 @@ You must:
Messages not following these rules will be deleted, the right to send messages may be revoked, and the access to the new members to the group may be temporarily restricted, to prevent re-joining under a different name - our imperfect group moderation does not have a better solution at the moment.
You can join an English-speaking users group if you want to ask any questions: [#SimpleX-Group-4](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2Fw2GlucRXtRVgYnbt_9ZP-kmt76DekxxS%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEA0tJhTyMGUxznwmjb7aT24P1I1Wry_iURTuhOFlMb1Eo%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22WoPxjFqGEDlVazECOSi2dg%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:
@@ -127,6 +127,7 @@ Join our translators to help SimpleX grow!
|🇫🇮 fi|Suomi | |[![android app](https://hosted.weblate.org/widgets/simplex-chat/fi/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/fi/)<br>[![ios app](https://hosted.weblate.org/widgets/simplex-chat/fi/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/fi/)|[![website](https://hosted.weblate.org/widgets/simplex-chat/fi/website/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/website/fi/)||
|🇫🇷 fr|Français |[ishi_sama](https://github.com/ishi-sama)|[![android app](https://hosted.weblate.org/widgets/simplex-chat/fr/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/fr/)<br>[![ios app](https://hosted.weblate.org/widgets/simplex-chat/fr/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/fr/)|[![website](https://hosted.weblate.org/widgets/simplex-chat/fr/website/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/website/fr/)|[](https://github.com/simplex-chat/simplex-chat/tree/master/docs/lang/fr)|
|🇮🇱 he|עִברִית | |[![android app](https://hosted.weblate.org/widgets/simplex-chat/he/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/he/)<br>-|||
|🇭🇺 hu|Magyar | |[![android app](https://hosted.weblate.org/widgets/simplex-chat/hu/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/hu/)<br>-|||
|🇮🇹 it|Italiano |[unbranched](https://github.com/unbranched)|[![android app](https://hosted.weblate.org/widgets/simplex-chat/it/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/it/)<br>[![ios app](https://hosted.weblate.org/widgets/simplex-chat/it/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/it/)|[![website](https://hosted.weblate.org/widgets/simplex-chat/it/website/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/website/it/)||
|🇯🇵 ja|日本語 | |[![android app](https://hosted.weblate.org/widgets/simplex-chat/ja/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/ja/)<br>[![ios app](https://hosted.weblate.org/widgets/simplex-chat/ja/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/ja/)|[![website](https://hosted.weblate.org/widgets/simplex-chat/ja/website/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/website/ja/)||
|🇳🇱 nl|Nederlands|[mika-nl](https://github.com/mika-nl)|[![android app](https://hosted.weblate.org/widgets/simplex-chat/nl/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/nl/)<br>[![ios app](https://hosted.weblate.org/widgets/simplex-chat/nl/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/nl/)|[![website](https://hosted.weblate.org/widgets/simplex-chat/nl/website/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/website/nl/)||
@@ -134,6 +135,7 @@ Join our translators to help SimpleX grow!
|🇧🇷 pt-BR|Português||[![android app](https://hosted.weblate.org/widgets/simplex-chat/pt_BR/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/pt_BR/)<br>-|[![website](https://hosted.weblate.org/widgets/simplex-chat/pt_BR/website/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/website/pt_BR/)||
|🇷🇺 ru|Русский ||[![android app](https://hosted.weblate.org/widgets/simplex-chat/ru/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/ru/)<br>[![ios app](https://hosted.weblate.org/widgets/simplex-chat/ru/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/ru/)|||
|🇹🇭 th|ภาษาไทย |[titapa-punpun](https://github.com/titapa-punpun)|[![android app](https://hosted.weblate.org/widgets/simplex-chat/th/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/th/)<br>[![ios app](https://hosted.weblate.org/widgets/simplex-chat/th/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/th/)|||
|🇹🇷 tr|Türkçe | |[![android app](https://hosted.weblate.org/widgets/simplex-chat/tr/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/tr/)<br>[![ios app](https://hosted.weblate.org/widgets/simplex-chat/tr/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/tr/)|||
|🇺🇦 uk|Українська| |[![android app](https://hosted.weblate.org/widgets/simplex-chat/uk/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/uk/)<br>[![ios app](https://hosted.weblate.org/widgets/simplex-chat/uk/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/uk/)|[![website](https://hosted.weblate.org/widgets/simplex-chat/uk/website/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/website/uk/)||
|🇨🇳 zh-CHS|简体中文|[sith-on-mars](https://github.com/sith-on-mars)<br><br>[Float-hu](https://github.com/Float-hu)|[![android app](https://hosted.weblate.org/widgets/simplex-chat/zh_Hans/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/zh_Hans/)<br>[![ios app](https://hosted.weblate.org/widgets/simplex-chat/zh_Hans/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/zh_Hans/)<br>&nbsp;|<br><br>[![website](https://hosted.weblate.org/widgets/simplex-chat/zh_Hans/website/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/website/zh_Hans/)||
@@ -232,6 +234,8 @@ You can use SimpleX with your own servers and still communicate with people usin
Recent and important updates:
[Jan 24, 2024. SimpleX Chat: free infrastructure from Linode, v5.5 released with private notes, group history and a simpler UX to connect.](./blog/20240124-simplex-chat-infrastructure-costs-v5-5-simplex-ux-private-notes-group-history.md)
[Nov 25, 2023. SimpleX Chat v5.4 released: link mobile and desktop apps via quantum resistant protocol, and much better groups](./blog/20231125-simplex-chat-v5-4-link-mobile-desktop-quantum-resistant-better-groups.md).
[Sep 25, 2023. SimpleX Chat v5.3 released: desktop app, local file encryption, improved groups and directory service](./blog/20230925-simplex-chat-v5-3-desktop-app-local-file-encryption-directory-service.md).
@@ -297,7 +301,7 @@ What is already implemented:
11. Transport isolation - different TCP connections and Tor circuits are used for traffic of different user profiles, optionally - for different contacts and group member connections.
12. Manual messaging queue rotations to move conversation to another SMP relay.
13. Sending end-to-end encrypted files using [XFTP protocol](https://simplex.chat/blog/20230301-simplex-file-transfer-protocol.html).
14. Local files encryption, except videos (to be added later).
14. Local files encryption.
We plan to add:
@@ -369,12 +373,13 @@ Please also join [#simplex-devs](https://simplex.chat/contact#/?v=1-2&smp=smp%3A
- ✅ Desktop client.
- ✅ Encryption of local files stored in the app.
- ✅ Using mobile profiles from the desktop app.
- ✅ Private notes.
- ✅ Improve sending videos (including encryption of locally stored videos).
- 🏗 Improve experience for the new users.
- 🏗 Post-quantum resistant key exchange in double ratchet protocol.
- 🏗 Large groups, communities and public channels.
- Message delivery relay for senders (to conceal IP address from the recipients' servers and to reduce the traffic).
- 🏗 Message delivery relay for senders (to conceal IP address from the recipients' servers and to reduce the traffic).
- Privacy & security slider - a simple way to set all settings at once.
- Improve sending videos (including encryption of locally stored videos).
- SMP queue redundancy and rotation (manual is supported).
- Include optional message into connection request sent via contact address.
- Improved navigation and search in the conversation (expand and scroll to quoted message, scroll to search results, etc.).

View File

@@ -15,6 +15,7 @@ class AppDelegate: NSObject, UIApplicationDelegate {
logger.debug("AppDelegate: didFinishLaunchingWithOptions")
application.registerForRemoteNotifications()
if #available(iOS 17.0, *) { trackKeyboard() }
NotificationCenter.default.addObserver(self, selector: #selector(pasteboardChanged), name: UIPasteboard.changedNotification, object: nil)
return true
}
@@ -36,12 +37,17 @@ class AppDelegate: NSObject, UIApplicationDelegate {
ChatModel.shared.keyboardHeight = 0
}
@objc func pasteboardChanged() {
ChatModel.shared.pasteboardHasStrings = UIPasteboard.general.hasStrings
}
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
let token = deviceToken.map { String(format: "%02hhx", $0) }.joined()
logger.debug("AppDelegate: didRegisterForRemoteNotificationsWithDeviceToken \(token)")
let m = ChatModel.shared
let deviceToken = DeviceToken(pushProvider: PushProvider(env: pushEnvironment), token: token)
m.deviceToken = deviceToken
// savedToken is set in startChat, when it is started before this method is called
if m.savedToken != nil {
registerToken(token: deviceToken)
}
@@ -80,7 +86,7 @@ class AppDelegate: NSObject, UIApplicationDelegate {
}
} else if let checkMessages = ntfData["checkMessages"] as? Bool, checkMessages {
logger.debug("AppDelegate: didReceiveRemoteNotification: checkMessages")
if appStateGroupDefault.get().inactive && m.ntfEnablePeriodic {
if m.ntfEnablePeriodic && allowBackgroundRefresh() && BGManager.shared.lastRanLongAgo {
receiveMessages(completionHandler)
} else {
completionHandler(.noData)

View File

@@ -14,11 +14,14 @@ struct ContentView: View {
@ObservedObject var alertManager = AlertManager.shared
@ObservedObject var callController = CallController.shared
@Environment(\.colorScheme) var colorScheme
@Binding var doAuthenticate: Bool
@Binding var userAuthorized: Bool?
@Binding var canConnectCall: Bool
@Binding var lastSuccessfulUnlock: TimeInterval?
@Binding var showInitializationView: Bool
var contentAccessAuthenticationExtended: Bool
@Environment(\.scenePhase) var scenePhase
@State private var automaticAuthenticationAttempted = false
@State private var canConnectViewCall = false
@State private var lastSuccessfulUnlock: TimeInterval? = nil
@AppStorage(DEFAULT_SHOW_LA_NOTICE) private var prefShowLANotice = false
@AppStorage(DEFAULT_LA_NOTICE_SHOWN) private var prefLANoticeShown = false
@AppStorage(DEFAULT_PERFORM_LA) private var prefPerformLA = false
@@ -28,6 +31,7 @@ struct ContentView: View {
@State private var showWhatsNew = false
@State private var showChooseLAMode = false
@State private var showSetPasscode = false
@State private var waitingForOrPassedAuth = true
@State private var chatListActionSheet: ChatListActionSheet? = nil
private enum ChatListActionSheet: Identifiable {
@@ -40,16 +44,31 @@ struct ContentView: View {
}
}
private var accessAuthenticated: Bool {
chatModel.contentViewAccessAuthenticated || contentAccessAuthenticationExtended
}
var body: some View {
ZStack {
contentView()
// contentView() has to be in a single branch, so that enabling authentication doesn't trigger re-rendering and close settings.
// i.e. with separate branches like this settings are closed: `if prefPerformLA { ... contentView() ... } else { contentView() }
if !prefPerformLA || accessAuthenticated {
contentView()
} else {
lockButton()
}
if chatModel.showCallView, let call = chatModel.activeCall {
callView(call)
}
if !showSettings, let la = chatModel.laRequest {
LocalAuthView(authRequest: la)
.onDisappear {
// this flag is separate from accessAuthenticated to show initializationView while we wait for authentication
waitingForOrPassedAuth = accessAuthenticated
}
} else if showSetPasscode {
SetAppPasscodeView {
chatModel.contentViewAccessAuthenticated = true
prefPerformLA = true
showSetPasscode = false
privacyLocalAuthModeDefault.set(.passcode)
@@ -59,15 +78,10 @@ struct ContentView: View {
showSetPasscode = false
alertManager.showAlert(laPasscodeNotSetAlert())
}
} else if chatModel.chatDbStatus == nil && AppChatState.shared.value != .stopped && waitingForOrPassedAuth {
initializationView()
}
}
.onAppear {
if prefPerformLA { requestNtfAuthorization() }
initAuthenticate()
}
.onChange(of: doAuthenticate) { _ in
initAuthenticate()
}
.alert(isPresented: $alertManager.presentAlert) { alertManager.alertView! }
.sheet(isPresented: $showSettings) {
SettingsView(showSettings: $showSettings)
@@ -76,14 +90,44 @@ struct ContentView: View {
Button("System authentication") { initialEnableLA() }
Button("Passcode entry") { showSetPasscode = true }
}
.onChange(of: scenePhase) { phase in
logger.debug("scenePhase was \(String(describing: scenePhase)), now \(String(describing: phase))")
switch (phase) {
case .background:
// also see .onChange(of: scenePhase) in SimpleXApp: on entering background
// it remembers enteredBackgroundAuthenticated and sets chatModel.contentViewAccessAuthenticated to false
automaticAuthenticationAttempted = false
canConnectViewCall = false
case .active:
canConnectViewCall = !prefPerformLA || contentAccessAuthenticationExtended || unlockedRecently()
// condition `!chatModel.contentViewAccessAuthenticated` is required for when authentication is enabled in settings or on initial notice
if prefPerformLA && !chatModel.contentViewAccessAuthenticated {
if AppChatState.shared.value != .stopped {
if contentAccessAuthenticationExtended {
chatModel.contentViewAccessAuthenticated = true
} else {
if !automaticAuthenticationAttempted {
automaticAuthenticationAttempted = true
// authenticate if call kit call is not in progress
if !(CallController.useCallKit() && chatModel.showCallView && chatModel.activeCall != nil) {
authenticateContentViewAccess()
}
}
}
} else {
// when app is stopped automatic authentication is not attempted
chatModel.contentViewAccessAuthenticated = contentAccessAuthenticationExtended
}
}
default:
break
}
}
}
@ViewBuilder private func contentView() -> some View {
if prefPerformLA && userAuthorized != true {
lockButton()
} else if chatModel.chatDbStatus == nil && showInitializationView {
initializationView()
} else if let status = chatModel.chatDbStatus, status != .ok {
if let status = chatModel.chatDbStatus, status != .ok {
DatabaseErrorView(status: status)
} else if !chatModel.v3DBMigration.startChat {
MigrateToAppGroupView()
@@ -106,11 +150,11 @@ struct ContentView: View {
if CallController.useCallKit() {
ActiveCallView(call: call, canConnectCall: Binding.constant(true))
.onDisappear {
if userAuthorized == false && doAuthenticate { runAuthenticate() }
if prefPerformLA && !accessAuthenticated { authenticateContentViewAccess() }
}
} else {
ActiveCallView(call: call, canConnectCall: $canConnectCall)
if prefPerformLA && userAuthorized != true {
ActiveCallView(call: call, canConnectCall: $canConnectViewCall)
if prefPerformLA && !accessAuthenticated {
Rectangle()
.fill(colorScheme == .dark ? .black : .white)
.frame(maxWidth: .infinity, maxHeight: .infinity)
@@ -120,24 +164,29 @@ struct ContentView: View {
}
private func lockButton() -> some View {
Button(action: runAuthenticate) { Label("Unlock", systemImage: "lock") }
Button(action: authenticateContentViewAccess) { Label("Unlock", systemImage: "lock") }
}
private func initializationView() -> some View {
VStack {
ProgressView().scaleEffect(2)
Text("Opening database")
Text("Opening app")
.padding()
}
.frame(maxWidth: .infinity, maxHeight: .infinity )
.background(
Rectangle()
.fill(.background)
)
}
private func mainView() -> some View {
ZStack(alignment: .top) {
ChatListView(showSettings: $showSettings).privacySensitive(protectScreen)
.onAppear {
if !prefPerformLA { requestNtfAuthorization() }
requestNtfAuthorization()
// Local Authentication notice is to be shown on next start after onboarding is complete
if (!prefLANoticeShown && prefShowLANotice && !chatModel.chats.isEmpty) {
if (!prefLANoticeShown && prefShowLANotice && chatModel.chats.count > 2) {
prefLANoticeShown = true
alertManager.showAlert(laNoticeAlert())
} else if !chatModel.showCallView && CallController.shared.activeCallInvitation == nil {
@@ -187,48 +236,37 @@ struct ContentView: View {
}
}
private func initAuthenticate() {
logger.debug("initAuthenticate")
if CallController.useCallKit() && chatModel.showCallView && chatModel.activeCall != nil {
userAuthorized = false
} else if doAuthenticate {
runAuthenticate()
}
}
private func runAuthenticate() {
logger.debug("DEBUGGING: runAuthenticate")
if !prefPerformLA {
userAuthorized = true
private func unlockedRecently() -> Bool {
if let lastSuccessfulUnlock = lastSuccessfulUnlock {
return ProcessInfo.processInfo.systemUptime - lastSuccessfulUnlock < 2
} else {
logger.debug("DEBUGGING: before dismissAllSheets")
dismissAllSheets(animated: false) {
logger.debug("DEBUGGING: in dismissAllSheets callback")
chatModel.chatId = nil
justAuthenticate()
}
return false
}
}
private func justAuthenticate() {
userAuthorized = false
let laMode = privacyLocalAuthModeDefault.get()
authenticate(reason: NSLocalizedString("Unlock app", comment: "authentication reason"), selfDestruct: true) { laResult in
logger.debug("DEBUGGING: authenticate callback: \(String(describing: laResult))")
switch (laResult) {
case .success:
userAuthorized = true
canConnectCall = true
lastSuccessfulUnlock = ProcessInfo.processInfo.systemUptime
case .failed:
if laMode == .passcode {
AlertManager.shared.showAlert(laFailedAlert())
private func authenticateContentViewAccess() {
logger.debug("DEBUGGING: authenticateContentViewAccess")
dismissAllSheets(animated: false) {
logger.debug("DEBUGGING: authenticateContentViewAccess, in dismissAllSheets callback")
chatModel.chatId = nil
authenticate(reason: NSLocalizedString("Unlock app", comment: "authentication reason"), selfDestruct: true) { laResult in
logger.debug("DEBUGGING: authenticate callback: \(String(describing: laResult))")
switch (laResult) {
case .success:
chatModel.contentViewAccessAuthenticated = true
canConnectViewCall = true
lastSuccessfulUnlock = ProcessInfo.processInfo.systemUptime
case .failed:
chatModel.contentViewAccessAuthenticated = false
if privacyLocalAuthModeDefault.get() == .passcode {
AlertManager.shared.showAlert(laFailedAlert())
}
case .unavailable:
prefPerformLA = false
canConnectViewCall = true
AlertManager.shared.showAlert(laUnavailableTurningOffAlert())
}
case .unavailable:
userAuthorized = true
prefPerformLA = false
canConnectCall = true
AlertManager.shared.showAlert(laUnavailableTurningOffAlert())
}
}
}
@@ -259,6 +297,7 @@ struct ContentView: View {
authenticate(reason: NSLocalizedString("Enable SimpleX Lock", comment: "authentication reason")) { laResult in
switch laResult {
case .success:
chatModel.contentViewAccessAuthenticated = true
prefPerformLA = true
alertManager.showAlert(laTurnedOnAlert())
case .failed:

View File

@@ -15,7 +15,13 @@ private let receiveTaskId = "chat.simplex.app.receive"
// TCP timeout + 2 sec
private let waitForMessages: TimeInterval = 6
private let bgRefreshInterval: TimeInterval = 450
// This is the smallest interval between refreshes, and also target interval in "off" mode
private let bgRefreshInterval: TimeInterval = 600 // 10 minutes
// This intervals are used for background refresh in instant and periodic modes
private let periodicBgRefreshInterval: TimeInterval = 1200 // 20 minutes
private let maxBgRefreshInterval: TimeInterval = 2400 // 40 minutes
private let maxTimerCount = 9
@@ -33,14 +39,14 @@ class BGManager {
}
}
func schedule() {
func schedule(interval: TimeInterval? = nil) {
if !ChatModel.shared.ntfEnableLocal {
logger.debug("BGManager.schedule: disabled")
return
}
logger.debug("BGManager.schedule")
let request = BGAppRefreshTaskRequest(identifier: receiveTaskId)
request.earliestBeginDate = Date(timeIntervalSinceNow: bgRefreshInterval)
request.earliestBeginDate = Date(timeIntervalSinceNow: interval ?? runInterval)
do {
try BGTaskScheduler.shared.submit(request)
} catch {
@@ -48,20 +54,34 @@ class BGManager {
}
}
var runInterval: TimeInterval {
switch ChatModel.shared.notificationMode {
case .instant: maxBgRefreshInterval
case .periodic: periodicBgRefreshInterval
case .off: bgRefreshInterval
}
}
var lastRanLongAgo: Bool {
Date.now.timeIntervalSince(chatLastBackgroundRunGroupDefault.get()) > runInterval
}
private func handleRefresh(_ task: BGAppRefreshTask) {
if !ChatModel.shared.ntfEnableLocal {
logger.debug("BGManager.handleRefresh: disabled")
return
}
logger.debug("BGManager.handleRefresh")
schedule()
if appStateGroupDefault.get().inactive {
let shouldRun_ = lastRanLongAgo
if allowBackgroundRefresh() && shouldRun_ {
schedule()
let completeRefresh = completionHandler {
task.setTaskCompleted(success: true)
}
task.expirationHandler = { completeRefresh("expirationHandler") }
receiveMessages(completeRefresh)
} else {
schedule(interval: shouldRun_ ? bgRefreshInterval : runInterval)
logger.debug("BGManager.completionHandler: already active, not started")
task.setTaskCompleted(success: true)
}
@@ -90,20 +110,22 @@ class BGManager {
}
self.completed = false
DispatchQueue.main.async {
chatLastBackgroundRunGroupDefault.set(Date.now)
let m = ChatModel.shared
if (!m.chatInitialized) {
setAppState(.bgRefresh)
do {
try initializeChat(start: true)
} catch let error {
fatalError("Failed to start or load chats: \(responseError(error))")
}
}
activateChat(appState: .bgRefresh)
if m.currentUser == nil {
completeReceiving("no current user")
return
}
logger.debug("BGManager.receiveMessages: starting chat")
activateChat(appState: .bgRefresh)
let cr = ChatReceiver()
self.chatReceiver = cr
cr.start()

View File

@@ -54,9 +54,13 @@ final class ChatModel: ObservableObject {
@Published var chatDbChanged = false
@Published var chatDbEncrypted: Bool?
@Published var chatDbStatus: DBMigrationResult?
@Published var ctrlInitInProgress: Bool = false
// local authentication
@Published var contentViewAccessAuthenticated: Bool = false
@Published var laRequest: LocalAuthRequest?
// list of chat "previews"
@Published var chats: [Chat] = []
@Published var deletedChats: Set<String> = []
// map of connections network statuses, key is agent connection id
@Published var networkStatuses: Dictionary<String, NetworkStatus> = [:]
// current chat
@@ -83,18 +87,19 @@ final class ChatModel: ObservableObject {
// current WebRTC call
@Published var callInvitations: Dictionary<ChatId, RcvCallInvitation> = [:]
@Published var activeCall: Call?
@Published var callCommand: WCallCommand?
let callCommand: WebRTCCommandProcessor = WebRTCCommandProcessor()
@Published var showCallView = false
// remote desktop
@Published var remoteCtrlSession: RemoteCtrlSession?
// currently showing QR code
@Published var connReqInv: String?
// currently showing invitation
@Published var showingInvitation: ShowingInvitation?
// audio recording and playback
@Published var stopPreviousRecPlay: URL? = nil // coordinates currently playing source
@Published var draft: ComposeState?
@Published var draftChatId: String?
// tracks keyboard height via subscription in AppDelegate
@Published var keyboardHeight: CGFloat = 0
@Published var pasteboardHasStrings: Bool = UIPasteboard.general.hasStrings
var messageDelivery: Dictionary<Int64, () -> Void> = [:]
@@ -104,12 +109,10 @@ final class ChatModel: ObservableObject {
static var ok: Bool { ChatModel.shared.chatDbStatus == .ok }
var ntfEnableLocal: Bool {
notificationMode == .off || ntfEnableLocalGroupDefault.get()
}
let ntfEnableLocal = true
var ntfEnablePeriodic: Bool {
notificationMode == .periodic || ntfEnablePeriodicGroupDefault.get()
notificationMode != .off
}
var activeRemoteCtrl: Bool {
@@ -136,7 +139,7 @@ final class ChatModel: ObservableObject {
}
func removeUser(_ user: User) {
if let i = getUserIndex(user), users[i].user.userId != currentUser?.userId {
if let i = getUserIndex(user) {
users.remove(at: i)
}
}
@@ -267,7 +270,20 @@ final class ChatModel: ObservableObject {
func addChatItem(_ cInfo: ChatInfo, _ cItem: ChatItem) {
// update previews
if let i = getChatIndex(cInfo.id) {
chats[i].chatItems = [cItem]
chats[i].chatItems = switch cInfo {
case .group:
if let currentPreviewItem = chats[i].chatItems.first {
if cItem.meta.itemTs >= currentPreviewItem.meta.itemTs {
[cItem]
} else {
[currentPreviewItem]
}
} else {
[cItem]
}
default:
[cItem]
}
if case .rcvNew = cItem.meta.itemStatus {
chats[i].chatStats.unreadCount = chats[i].chatStats.unreadCount + 1
increaseUnreadCounter(user: currentUser!)
@@ -607,14 +623,16 @@ final class ChatModel: ObservableObject {
}
func dismissConnReqView(_ id: String) {
if let connReqInv = connReqInv,
let c = getChat(id),
case let .contactConnection(contactConnection) = c.chatInfo,
connReqInv == contactConnection.connReqInv {
if id == showingInvitation?.connId {
markShowingInvitationUsed()
dismissAllSheets()
}
}
func markShowingInvitationUsed() {
showingInvitation?.connChatUsed = true
}
func removeChat(_ id: String) {
withAnimation {
chats.removeAll(where: { $0.id == id })
@@ -691,6 +709,11 @@ final class ChatModel: ObservableObject {
}
}
struct ShowingInvitation {
var connId: String
var connChatUsed: Bool
}
struct NTFContactRequest {
var incognito: Bool
var chatId: String
@@ -733,6 +756,8 @@ final class Chat: ObservableObject, Identifiable {
case let .group(groupInfo):
let m = groupInfo.membership
return m.memberActive && m.memberRole >= .member
case .local:
return true
default: return false
}
}

View File

@@ -158,7 +158,8 @@ func imageHasAlpha(_ img: UIImage) -> Bool {
return false
}
func saveFileFromURL(_ url: URL, encrypted: Bool) -> CryptoFile? {
func saveFileFromURL(_ url: URL) -> CryptoFile? {
let encrypted = privacyEncryptLocalFilesGroupDefault.get()
let savedFile: CryptoFile?
if url.startAccessingSecurityScopedResource() {
do {
@@ -185,28 +186,37 @@ func saveFileFromURL(_ url: URL, encrypted: Bool) -> CryptoFile? {
func moveTempFileFromURL(_ url: URL) -> CryptoFile? {
do {
let encrypted = privacyEncryptLocalFilesGroupDefault.get()
let fileName = uniqueCombine(url.lastPathComponent)
try FileManager.default.moveItem(at: url, to: getAppFilePath(fileName))
let savedFile: CryptoFile?
if encrypted {
let cfArgs = try encryptCryptoFile(fromPath: url.path, toPath: getAppFilePath(fileName).path)
try FileManager.default.removeItem(atPath: url.path)
savedFile = CryptoFile(filePath: fileName, cryptoArgs: cfArgs)
} else {
try FileManager.default.moveItem(at: url, to: getAppFilePath(fileName))
savedFile = CryptoFile.plain(fileName)
}
ChatModel.shared.filesToDelete.remove(url)
return CryptoFile.plain(fileName)
return savedFile
} catch {
logger.error("ImageUtils.moveTempFileFromURL error: \(error.localizedDescription)")
return nil
}
}
func generateNewFileName(_ prefix: String, _ ext: String) -> String {
uniqueCombine("\(prefix)_\(getTimestamp()).\(ext)")
func generateNewFileName(_ prefix: String, _ ext: String, fullPath: Bool = false) -> String {
uniqueCombine("\(prefix)_\(getTimestamp()).\(ext)", fullPath: fullPath)
}
private func uniqueCombine(_ fileName: String) -> String {
private func uniqueCombine(_ fileName: String, fullPath: Bool = false) -> String {
func tryCombine(_ fileName: String, _ n: Int) -> String {
let ns = fileName as NSString
let name = ns.deletingPathExtension
let ext = ns.pathExtension
let suffix = (n == 0) ? "" : "_\(n)"
let f = "\(name)\(suffix).\(ext)"
return (FileManager.default.fileExists(atPath: getAppFilePath(f).path)) ? tryCombine(fileName, n + 1) : f
return (FileManager.default.fileExists(atPath: fullPath ? f : getAppFilePath(f).path)) ? tryCombine(fileName, n + 1) : f
}
return tryCombine(fileName, 0)
}

View File

@@ -0,0 +1,83 @@
//
// NSESubscriber.swift
// SimpleXChat
//
// Created by Evgeny on 09/12/2023.
// Copyright © 2023 SimpleX Chat. All rights reserved.
//
import Foundation
import SimpleXChat
private var nseSubscribers: [UUID:NSESubscriber] = [:]
// timeout for active notification service extension going into "suspending" state.
// If in two seconds the state does not change, we assume that it was not running and proceed with app activation/answering call.
private let SUSPENDING_TIMEOUT: TimeInterval = 2
// timeout should be larger than SUSPENDING_TIMEOUT
func waitNSESuspended(timeout: TimeInterval, suspended: @escaping (Bool) -> Void) {
if timeout <= SUSPENDING_TIMEOUT {
logger.warning("waitNSESuspended: small timeout \(timeout), using \(SUSPENDING_TIMEOUT + 1)")
}
var state = nseStateGroupDefault.get()
if case .suspended = state {
DispatchQueue.main.async { suspended(true) }
return
}
let id = UUID()
var suspendedCalled = false
checkTimeout()
nseSubscribers[id] = nseMessageSubscriber { msg in
if case let .state(newState) = msg {
state = newState
logger.debug("waitNSESuspended state: \(state.rawValue)")
if case .suspended = newState {
notifySuspended(true)
}
}
}
return
func notifySuspended(_ ok: Bool) {
logger.debug("waitNSESuspended notifySuspended: \(ok)")
if !suspendedCalled {
logger.debug("waitNSESuspended notifySuspended: calling suspended(\(ok))")
suspendedCalled = true
nseSubscribers.removeValue(forKey: id)
DispatchQueue.main.async { suspended(ok) }
}
}
func checkTimeout() {
if !suspending() {
checkSuspendingTimeout()
} else if state == .suspending {
checkSuspendedTimeout()
}
}
func suspending() -> Bool {
suspendedCalled || state == .suspended || state == .suspending
}
func checkSuspendingTimeout() {
DispatchQueue.global().asyncAfter(deadline: .now() + SUSPENDING_TIMEOUT) {
logger.debug("waitNSESuspended check suspending timeout")
if !suspending() {
notifySuspended(false)
} else if state != .suspended {
checkSuspendedTimeout()
}
}
}
func checkSuspendedTimeout() {
DispatchQueue.global().asyncAfter(deadline: .now() + min(timeout - SUSPENDING_TIMEOUT, 1)) {
logger.debug("waitNSESuspended check suspended timeout")
if state != .suspended {
notifySuspended(false)
}
}
}
}

View File

@@ -211,7 +211,7 @@ func apiDeleteUser(_ userId: Int64, _ delSMPQueues: Bool, viewPwd: String?) asyn
}
func apiStartChat() throws -> Bool {
let r = chatSendCmdSync(.startChat(subscribe: true, expire: true, xftp: true))
let r = chatSendCmdSync(.startChat(mainApp: true))
switch r {
case .chatStarted: return true
case .chatRunning: return false
@@ -228,7 +228,8 @@ func apiStopChat() async throws {
}
func apiActivateChat() {
let r = chatSendCmdSync(.apiActivateChat)
chatReopenStore()
let r = chatSendCmdSync(.apiActivateChat(restoreChat: true))
if case .cmdOk = r { return }
logger.error("apiActivateChat error: \(String(describing: r))")
}
@@ -364,6 +365,13 @@ func apiSendMessage(type: ChatType, id: Int64, file: CryptoFile?, quotedItemId:
}
}
func apiCreateChatItem(noteFolderId: Int64, file: CryptoFile?, msg: MsgContent) async -> ChatItem? {
let r = await chatSendCmd(.apiCreateChatItem(noteFolderId: noteFolderId, file: file, msg: msg))
if case let .newChatItem(_, aChatItem) = r { return aChatItem.chatItem }
createChatItemErrorAlert(r)
return nil
}
private func sendMessageErrorAlert(_ r: ChatResponse) {
logger.error("apiSendMessage error: \(String(describing: r))")
AlertManager.shared.showAlertMsg(
@@ -372,6 +380,14 @@ private func sendMessageErrorAlert(_ r: ChatResponse) {
)
}
private func createChatItemErrorAlert(_ r: ChatResponse) {
logger.error("apiCreateChatItem error: \(String(describing: r))")
AlertManager.shared.showAlertMsg(
title: "Error creating message",
message: "Error: \(String(describing: r))"
)
}
func apiUpdateChatItem(type: ChatType, id: Int64, itemId: Int64, msg: MsgContent, live: Bool = false) async throws -> ChatItem {
let r = await chatSendCmd(.apiUpdateChatItem(type: type, id: id, itemId: itemId, msg: msg, live: live), bgDelay: msgDelay)
if case let .chatItemUpdated(_, aChatItem) = r { return aChatItem.chatItem }
@@ -402,7 +418,7 @@ func apiGetNtfToken() -> (DeviceToken?, NtfTknStatus?, NotificationsMode) {
case let .ntfToken(token, status, ntfMode): return (token, status, ntfMode)
case .chatCmdError(_, .errorAgent(.CMD(.PROHIBITED))): return (nil, nil, .off)
default:
logger.debug("apiGetNtfToken response: \(String(describing: r), privacy: .public)")
logger.debug("apiGetNtfToken response: \(String(describing: r))")
return (nil, nil, .off)
}
}
@@ -580,15 +596,15 @@ func apiVerifyGroupMember(_ groupId: Int64, _ groupMemberId: Int64, connectionCo
return nil
}
func apiAddContact(incognito: Bool) async -> (String, PendingContactConnection)? {
func apiAddContact(incognito: Bool) async -> ((String, PendingContactConnection)?, Alert?) {
guard let userId = ChatModel.shared.currentUser?.userId else {
logger.error("apiAddContact: no current user")
return nil
return (nil, nil)
}
let r = await chatSendCmd(.apiAddContact(userId: userId, incognito: incognito), bgTask: false)
if case let .invitation(_, connReqInvitation, connection) = r { return (connReqInvitation, connection) }
AlertManager.shared.showAlert(connectionErrorAlert(r))
return nil
if case let .invitation(_, connReqInvitation, connection) = r { return ((connReqInvitation, connection), nil) }
let alert = connectionErrorAlert(r)
return (nil, alert)
}
func apiSetConnectionIncognito(connId: Int64, incognito: Bool) async throws -> PendingContactConnection? {
@@ -605,27 +621,29 @@ func apiConnectPlan(connReq: String) async throws -> ConnectionPlan {
throw r
}
func apiConnect(incognito: Bool, connReq: String) async -> ConnReqType? {
let (connReqType, alert) = await apiConnect_(incognito: incognito, connReq: connReq)
func apiConnect(incognito: Bool, connReq: String) async -> (ConnReqType, PendingContactConnection)? {
let (r, alert) = await apiConnect_(incognito: incognito, connReq: connReq)
if let alert = alert {
AlertManager.shared.showAlert(alert)
return nil
} else {
return connReqType
return r
}
}
func apiConnect_(incognito: Bool, connReq: String) async -> (ConnReqType?, Alert?) {
func apiConnect_(incognito: Bool, connReq: String) async -> ((ConnReqType, PendingContactConnection)?, Alert?) {
guard let userId = ChatModel.shared.currentUser?.userId else {
logger.error("apiConnect: no current user")
return (nil, nil)
}
let r = await chatSendCmd(.apiConnect(userId: userId, incognito: incognito, connReq: connReq))
let m = ChatModel.shared
switch r {
case .sentConfirmation: return (.invitation, nil)
case .sentInvitation: return (.contact, nil)
case let .sentConfirmation(_, connection):
return ((.invitation, connection), nil)
case let .sentInvitation(_, connection):
return ((.contact, connection), nil)
case let .contactAlreadyExists(_, contact):
let m = ChatModel.shared
if let c = m.getContactChat(contact.contactId) {
await MainActor.run { m.chatId = c.id }
}
@@ -688,6 +706,9 @@ func apiConnectContactViaAddress(incognito: Bool, contactId: Int64) async -> (Co
}
func apiDeleteChat(type: ChatType, id: Int64, notify: Bool? = nil) async throws {
let chatId = type.rawValue + id.description
DispatchQueue.main.async { ChatModel.shared.deletedChats.insert(chatId) }
defer { DispatchQueue.main.async { ChatModel.shared.deletedChats.remove(chatId) } }
let r = await chatSendCmd(.apiDeleteChat(type: type, id: id, notify: notify), bgTask: false)
if case .direct = type, case .contactDeleted = r { return }
if case .contactConnection = type, case .contactConnectionDeleted = r { return }
@@ -849,8 +870,8 @@ func apiChatUnread(type: ChatType, id: Int64, unreadChat: Bool) async throws {
try await sendCommandOkResp(.apiChatUnread(type: type, id: id, unreadChat: unreadChat))
}
func receiveFile(user: any UserLike, fileId: Int64, encrypted: Bool, auto: Bool = false) async {
if let chatItem = await apiReceiveFile(fileId: fileId, encrypted: encrypted, auto: auto) {
func receiveFile(user: any UserLike, fileId: Int64, auto: Bool = false) async {
if let chatItem = await apiReceiveFile(fileId: fileId, encrypted: privacyEncryptLocalFilesGroupDefault.get(), auto: auto) {
await chatItemSimpleUpdate(user, chatItem)
}
}
@@ -1120,6 +1141,12 @@ func apiMemberRole(_ groupId: Int64, _ memberId: Int64, _ memberRole: GroupMembe
throw r
}
func apiBlockMemberForAll(_ groupId: Int64, _ memberId: Int64, _ blocked: Bool) async throws -> GroupMember {
let r = await chatSendCmd(.apiBlockMemberForAll(groupId: groupId, memberId: memberId, blocked: blocked), bgTask: false)
if case let .memberBlockedForAllUser(_, _, member, _) = r { return member }
throw r
}
func leaveGroup(_ groupId: Int64) async {
do {
let groupInfo = try await apiLeaveGroup(groupId)
@@ -1145,7 +1172,7 @@ func filterMembersToAdd(_ ms: [GMember]) -> [Contact] {
let memberContactIds = ms.compactMap{ m in m.wrapped.memberCurrent ? m.wrapped.memberContactId : nil }
return ChatModel.shared.chats
.compactMap{ $0.chatInfo.contact }
.filter{ !memberContactIds.contains($0.apiId) }
.filter{ c in c.ready && c.active && !memberContactIds.contains(c.apiId) }
.sorted{ $0.displayName.lowercased() < $1.displayName.lowercased() }
}
@@ -1209,9 +1236,11 @@ private func currentUserId(_ funcName: String) throws -> Int64 {
throw RuntimeError("\(funcName): no current user")
}
func initializeChat(start: Bool, dbKey: String? = nil, refreshInvitations: Bool = true, confirmMigrations: MigrationConfirmation? = nil) throws {
func initializeChat(start: Bool, confirmStart: Bool = false, dbKey: String? = nil, refreshInvitations: Bool = true, confirmMigrations: MigrationConfirmation? = nil) throws {
logger.debug("initializeChat")
let m = ChatModel.shared
m.ctrlInitInProgress = true
defer { m.ctrlInitInProgress = false }
(m.chatDbEncrypted, m.chatDbStatus) = chatMigrateInit(dbKey, confirmMigrations: confirmMigrations)
if m.chatDbStatus != .ok { return }
// If we migrated successfully means previous re-encryption process on database level finished successfully too
@@ -1228,10 +1257,43 @@ func initializeChat(start: Bool, dbKey: String? = nil, refreshInvitations: Bool
onboardingStageDefault.set(.step1_SimpleXInfo)
privacyDeliveryReceiptsSet.set(true)
m.onboardingStage = .step1_SimpleXInfo
} else if start {
} else if confirmStart {
showStartChatAfterRestartAlert { start in
do {
if start { AppChatState.shared.set(.active) }
try chatInitialized(start: start, refreshInvitations: refreshInvitations)
} catch let error {
logger.error("ChatInitialized error: \(error)")
}
}
} else {
try chatInitialized(start: start, refreshInvitations: refreshInvitations)
}
}
func showStartChatAfterRestartAlert(result: @escaping (_ start: Bool) -> Void) {
AlertManager.shared.showAlert(Alert(
title: Text("Start chat?"),
message: Text("Chat is stopped. If you already used this database on another device, you should transfer it back before starting chat."),
primaryButton: .default(Text("Ok")) {
result(true)
},
secondaryButton: .cancel {
result(false)
}
))
}
private func chatInitialized(start: Bool, refreshInvitations: Bool) throws {
let m = ChatModel.shared
if m.currentUser == nil { return }
if start {
try startChat(refreshInvitations: refreshInvitations)
} else {
m.chatRunning = false
try getUserChatData()
NtfManager.shared.setNtfBadgeCount(m.totalUnreadCountForAllUsers())
m.onboardingStage = onboardingStageDefault.get()
}
}
@@ -1248,6 +1310,8 @@ func startChat(refreshInvitations: Bool = true) throws {
try refreshCallInvitations()
}
(m.savedToken, m.tokenStatus, m.notificationMode) = apiGetNtfToken()
// deviceToken is set when AppDelegate.application(didRegisterForRemoteNotificationsWithDeviceToken:) is called,
// when it is called before startChat
if let token = m.deviceToken {
registerToken(token: token)
}
@@ -1281,8 +1345,12 @@ private func changeActiveUser_(_ userId: Int64, viewPwd: String?) throws {
try getUserChatData()
}
func changeActiveUserAsync_(_ userId: Int64, viewPwd: String?) async throws {
let currentUser = try await apiSetActiveUserAsync(userId, viewPwd: viewPwd)
func changeActiveUserAsync_(_ userId: Int64?, viewPwd: String?) async throws {
let currentUser = if let userId = userId {
try await apiSetActiveUserAsync(userId, viewPwd: viewPwd)
} else {
try apiGetActiveUser()
}
let users = try await listUsersAsync()
await MainActor.run {
let m = ChatModel.shared
@@ -1291,7 +1359,7 @@ func changeActiveUserAsync_(_ userId: Int64, viewPwd: String?) async throws {
}
try await getUserChatDataAsync()
await MainActor.run {
if var (_, invitation) = ChatModel.shared.callInvitations.first(where: { _, inv in inv.user.userId == userId }) {
if let currentUser = currentUser, var (_, invitation) = ChatModel.shared.callInvitations.first(where: { _, inv in inv.user.userId == userId }) {
invitation.user = currentUser
activateCall(invitation)
}
@@ -1307,14 +1375,21 @@ func getUserChatData() throws {
}
private func getUserChatDataAsync() async throws {
let userAddress = try await apiGetUserAddressAsync()
let chatItemTTL = try await getChatItemTTLAsync()
let chats = try await apiGetChatsAsync()
await MainActor.run {
let m = ChatModel.shared
m.userAddress = userAddress
m.chatItemTTL = chatItemTTL
m.chats = chats.map { Chat.init($0) }
let m = ChatModel.shared
if m.currentUser != nil {
let userAddress = try await apiGetUserAddressAsync()
let chatItemTTL = try await getChatItemTTLAsync()
let chats = try await apiGetChatsAsync()
await MainActor.run {
m.userAddress = userAddress
m.chatItemTTL = chatItemTTL
m.chats = chats.map { Chat.init($0) }
}
} else {
await MainActor.run {
m.userAddress = nil
m.chats = []
}
}
}
@@ -1362,18 +1437,6 @@ func processReceivedMsg(_ res: ChatResponse) async {
let m = ChatModel.shared
logger.debug("processReceivedMsg: \(res.responseType)")
switch res {
case let .newContactConnection(user, connection):
if active(user) {
await MainActor.run {
m.updateContactConnection(connection)
}
}
case let .contactConnectionDeleted(user, connection):
if active(user) {
await MainActor.run {
m.removeChat(connection.id)
}
}
case let .contactDeletedByContact(user, contact):
if active(user) && contact.directOrUsed {
await MainActor.run {
@@ -1485,7 +1548,7 @@ func processReceivedMsg(_ res: ChatResponse) async {
}
if let file = cItem.autoReceiveFile() {
Task {
await receiveFile(user: user, fileId: file.fileId, encrypted: cItem.encryptLocalFile, auto: true)
await receiveFile(user: user, fileId: file.fileId, auto: true)
}
}
if cItem.showNotification {
@@ -1623,6 +1686,13 @@ func processReceivedMsg(_ res: ChatResponse) async {
_ = m.upsertGroupMember(groupInfo, member)
}
}
case let .memberBlockedForAll(user, groupInfo, byMember: _, member: member, blocked: _):
if active(user) {
await MainActor.run {
m.updateGroup(groupInfo)
_ = m.upsertGroupMember(groupInfo, member)
}
}
case let .newMemberContactReceivedInv(user, contact, _, _):
if active(user) {
await MainActor.run {
@@ -1666,36 +1736,40 @@ func processReceivedMsg(_ res: ChatResponse) async {
activateCall(invitation)
case let .callOffer(_, contact, callType, offer, sharedKey, _):
await withCall(contact) { call in
call.callState = .offerReceived
call.peerMedia = callType.media
call.sharedKey = sharedKey
await MainActor.run {
call.callState = .offerReceived
call.peerMedia = callType.media
call.sharedKey = sharedKey
}
let useRelay = UserDefaults.standard.bool(forKey: DEFAULT_WEBRTC_POLICY_RELAY)
let iceServers = getIceServers()
logger.debug(".callOffer useRelay \(useRelay)")
logger.debug(".callOffer iceServers \(String(describing: iceServers))")
m.callCommand = .offer(
await m.callCommand.processCommand(.offer(
offer: offer.rtcSession,
iceCandidates: offer.rtcIceCandidates,
media: callType.media, aesKey: sharedKey,
iceServers: iceServers,
relay: useRelay
)
))
}
case let .callAnswer(_, contact, answer):
await withCall(contact) { call in
call.callState = .answerReceived
m.callCommand = .answer(answer: answer.rtcSession, iceCandidates: answer.rtcIceCandidates)
await MainActor.run {
call.callState = .answerReceived
}
await m.callCommand.processCommand(.answer(answer: answer.rtcSession, iceCandidates: answer.rtcIceCandidates))
}
case let .callExtraInfo(_, contact, extraInfo):
await withCall(contact) { _ in
m.callCommand = .ice(iceCandidates: extraInfo.rtcIceCandidates)
await m.callCommand.processCommand(.ice(iceCandidates: extraInfo.rtcIceCandidates))
}
case let .callEnded(_, contact):
if let invitation = await MainActor.run(body: { m.callInvitations.removeValue(forKey: contact.id) }) {
CallController.shared.reportCallRemoteEnded(invitation: invitation)
}
await withCall(contact) { call in
m.callCommand = .end
await m.callCommand.processCommand(.end)
CallController.shared.reportCallRemoteEnded(call: call)
}
case .chatSuspended:
@@ -1742,7 +1816,9 @@ func processReceivedMsg(_ res: ChatResponse) async {
// This delay is needed to cancel the session that fails on network failure,
// e.g. when user did not grant permission to access local network yet.
if let sess = m.remoteCtrlSession {
m.remoteCtrlSession = nil
await MainActor.run {
m.remoteCtrlSession = nil
}
if case .connected = sess.sessionState {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
switchToLocalSession()
@@ -1753,9 +1829,9 @@ func processReceivedMsg(_ res: ChatResponse) async {
logger.debug("unsupported event: \(res.responseType)")
}
func withCall(_ contact: Contact, _ perform: (Call) -> Void) async {
func withCall(_ contact: Contact, _ perform: (Call) async -> Void) async {
if let call = m.activeCall, call.contact.apiId == contact.apiId {
await MainActor.run { perform(call) }
await perform(call)
} else {
logger.debug("processReceivedMsg: ignoring \(res.responseType), not in call with the contact \(contact.id)")
}

View File

@@ -9,27 +9,30 @@
import Foundation
import UIKit
import SimpleXChat
import SwiftUI
private let suspendLockQueue = DispatchQueue(label: "chat.simplex.app.suspend.lock")
let appSuspendTimeout: Int = 15 // seconds
let bgSuspendTimeout: Int = 5 // seconds
let terminationTimeout: Int = 3 // seconds
let activationDelay: TimeInterval = 1.5
let nseSuspendTimeout: TimeInterval = 5
private func _suspendChat(timeout: Int) {
// this is a redundant check to prevent logical errors, like the one fixed in this PR
let state = appStateGroupDefault.get()
let state = AppChatState.shared.value
if !state.canSuspend {
logger.error("_suspendChat called, current state: \(state.rawValue, privacy: .public)")
logger.error("_suspendChat called, current state: \(state.rawValue)")
} else if ChatModel.ok {
appStateGroupDefault.set(.suspending)
AppChatState.shared.set(.suspending)
apiSuspendChat(timeoutMicroseconds: timeout * 1000000)
let endTask = beginBGTask(chatSuspended)
DispatchQueue.global().asyncAfter(deadline: .now() + Double(timeout) + 1, execute: endTask)
} else {
appStateGroupDefault.set(.suspended)
AppChatState.shared.set(.suspended)
}
}
@@ -41,18 +44,16 @@ func suspendChat() {
func suspendBgRefresh() {
suspendLockQueue.sync {
if case .bgRefresh = appStateGroupDefault.get() {
if case .bgRefresh = AppChatState.shared.value {
_suspendChat(timeout: bgSuspendTimeout)
}
}
}
private var terminating = false
func terminateChat() {
logger.debug("terminateChat")
suspendLockQueue.sync {
switch appStateGroupDefault.get() {
switch AppChatState.shared.value {
case .suspending:
// suspend instantly if already suspending
_chatSuspended()
@@ -64,7 +65,6 @@ func terminateChat() {
case .stopped:
chatCloseStore()
default:
terminating = true
// the store will be closed in _chatSuspended when event is received
_suspendChat(timeout: terminationTimeout)
}
@@ -73,7 +73,7 @@ func terminateChat() {
func chatSuspended() {
suspendLockQueue.sync {
if case .suspending = appStateGroupDefault.get() {
if case .suspending = AppChatState.shared.value {
_chatSuspended()
}
}
@@ -81,48 +81,111 @@ func chatSuspended() {
private func _chatSuspended() {
logger.debug("_chatSuspended")
appStateGroupDefault.set(.suspended)
AppChatState.shared.set(.suspended)
if ChatModel.shared.chatRunning == true {
ChatReceiver.shared.stop()
}
if terminating {
chatCloseStore()
chatCloseStore()
}
func setAppState(_ appState: AppState) {
suspendLockQueue.sync {
AppChatState.shared.set(appState)
}
}
func activateChat(appState: AppState = .active) {
logger.debug("DEBUGGING: activateChat")
terminating = false
suspendLockQueue.sync {
appStateGroupDefault.set(appState)
AppChatState.shared.set(appState)
if ChatModel.ok { apiActivateChat() }
logger.debug("DEBUGGING: activateChat: after apiActivateChat")
}
}
func initChatAndMigrate(refreshInvitations: Bool = true) {
terminating = false
let m = ChatModel.shared
if (!m.chatInitialized) {
m.v3DBMigration = v3DBMigrationDefault.get()
if AppChatState.shared.value == .stopped && storeDBPassphraseGroupDefault.get() && kcDatabasePassword.get() != nil {
initialize(start: true, confirmStart: true)
} else {
initialize(start: true)
}
}
func initialize(start: Bool, confirmStart: Bool = false) {
do {
m.v3DBMigration = v3DBMigrationDefault.get()
try initializeChat(start: m.v3DBMigration.startChat, refreshInvitations: refreshInvitations)
try initializeChat(start: m.v3DBMigration.startChat && start, confirmStart: m.v3DBMigration.startChat && confirmStart, refreshInvitations: refreshInvitations)
} catch let error {
fatalError("Failed to start or load chats: \(responseError(error))")
AlertManager.shared.showAlertMsg(
title: start ? "Error starting chat" : "Error opening chat",
message: "Please contact developers.\nError: \(responseError(error))"
)
}
}
}
func startChatAndActivate() {
terminating = false
func startChatForCall() {
logger.debug("DEBUGGING: startChatForCall")
if ChatModel.shared.chatRunning == true {
ChatReceiver.shared.start()
logger.debug("DEBUGGING: startChatForCall: after ChatReceiver.shared.start")
}
if .active != AppChatState.shared.value {
logger.debug("DEBUGGING: startChatForCall: before activateChat")
activateChat()
logger.debug("DEBUGGING: startChatForCall: after activateChat")
}
}
func startChatAndActivate(_ completion: @escaping () -> Void) {
logger.debug("DEBUGGING: startChatAndActivate")
if ChatModel.shared.chatRunning == true {
ChatReceiver.shared.start()
logger.debug("DEBUGGING: startChatAndActivate: after ChatReceiver.shared.start")
}
if .active != appStateGroupDefault.get() {
if case .active = AppChatState.shared.value {
completion()
} else if nseStateGroupDefault.get().inactive {
activate()
} else {
// setting app state to "activating" to notify NSE that it should suspend
setAppState(.activating)
waitNSESuspended(timeout: nseSuspendTimeout) { ok in
if !ok {
// if for some reason NSE failed to suspend,
// e.g., it crashed previously without setting its state to "suspended",
// set it to "suspended" state anyway, so that next time app
// does not have to wait when activating.
nseStateGroupDefault.set(.suspended)
}
if AppChatState.shared.value == .activating {
activate()
}
}
}
func activate() {
logger.debug("DEBUGGING: startChatAndActivate: before activateChat")
activateChat()
completion()
logger.debug("DEBUGGING: startChatAndActivate: after activateChat")
}
}
// appStateGroupDefault must not be used in the app directly, only via this singleton
class AppChatState {
static let shared = AppChatState()
private var value_ = appStateGroupDefault.get()
var value: AppState {
value_
}
func set(_ state: AppState) {
appStateGroupDefault.set(state)
sendAppState(state)
value_ = state
}
}

View File

@@ -16,14 +16,9 @@ struct SimpleXApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
@StateObject private var chatModel = ChatModel.shared
@ObservedObject var alertManager = AlertManager.shared
@Environment(\.scenePhase) var scenePhase
@AppStorage(DEFAULT_PERFORM_LA) private var prefPerformLA = false
@State private var userAuthorized: Bool?
@State private var doAuthenticate = false
@State private var enteredBackground: TimeInterval? = nil
@State private var canConnectCall = false
@State private var lastSuccessfulUnlock: TimeInterval? = nil
@State private var showInitializationView = false
@State private var enteredBackgroundAuthenticated: TimeInterval? = nil
init() {
DispatchQueue.global(qos: .background).sync {
@@ -39,53 +34,55 @@ struct SimpleXApp: App {
}
var body: some Scene {
return WindowGroup {
ContentView(
doAuthenticate: $doAuthenticate,
userAuthorized: $userAuthorized,
canConnectCall: $canConnectCall,
lastSuccessfulUnlock: $lastSuccessfulUnlock,
showInitializationView: $showInitializationView
)
WindowGroup {
// contentAccessAuthenticationExtended has to be passed to ContentView on view initialization,
// so that it's computed by the time view renders, and not on event after rendering
ContentView(contentAccessAuthenticationExtended: !authenticationExpired())
.environmentObject(chatModel)
.onOpenURL { url in
logger.debug("ContentView.onOpenURL: \(url)")
chatModel.appOpenUrl = url
}
.onAppear() {
showInitializationView = true
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
initChatAndMigrate()
if kcAppPassword.get() == nil || kcSelfDestructPassword.get() == nil {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) {
initChatAndMigrate()
}
}
}
.onChange(of: scenePhase) { phase in
logger.debug("scenePhase was \(String(describing: scenePhase)), now \(String(describing: phase))")
switch (phase) {
case .background:
// --- authentication
// see ContentView .onChange(of: scenePhase) for remaining authentication logic
if chatModel.contentViewAccessAuthenticated {
enteredBackgroundAuthenticated = ProcessInfo.processInfo.systemUptime
}
chatModel.contentViewAccessAuthenticated = false
// authentication ---
if CallController.useCallKit() && chatModel.activeCall != nil {
CallController.shared.shouldSuspendChat = true
} else {
suspendChat()
BGManager.shared.schedule()
}
if userAuthorized == true {
enteredBackground = ProcessInfo.processInfo.systemUptime
}
doAuthenticate = false
canConnectCall = false
NtfManager.shared.setNtfBadgeCount(chatModel.totalUnreadCountForAllUsers())
case .active:
CallController.shared.shouldSuspendChat = false
let appState = appStateGroupDefault.get()
startChatAndActivate()
if appState.inactive && chatModel.chatRunning == true {
updateChats()
if !chatModel.showCallView && !CallController.shared.hasActiveCalls() {
updateCallInvitations()
let appState = AppChatState.shared.value
if appState != .stopped {
startChatAndActivate {
if appState.inactive && chatModel.chatRunning == true {
updateChats()
if !chatModel.showCallView && !CallController.shared.hasActiveCalls() {
updateCallInvitations()
}
}
}
}
doAuthenticate = authenticationExpired()
canConnectCall = !(doAuthenticate && prefPerformLA) || unlockedRecently()
default:
break
}
@@ -103,12 +100,12 @@ struct SimpleXApp: App {
if legacyDatabase, case .documents = dbContainerGroupDefault.get() {
dbContainerGroupDefault.set(.documents)
setMigrationState(.offer)
logger.debug("SimpleXApp init: using legacy DB in documents folder: \(getAppDatabasePath(), privacy: .public)*.db")
logger.debug("SimpleXApp init: using legacy DB in documents folder: \(getAppDatabasePath())*.db")
} else {
dbContainerGroupDefault.set(.group)
setMigrationState(.ready)
logger.debug("SimpleXApp init: using DB in app group container: \(getAppDatabasePath(), privacy: .public)*.db")
logger.debug("SimpleXApp init: legacy DB\(legacyDatabase ? "" : " not", privacy: .public) present")
logger.debug("SimpleXApp init: using DB in app group container: \(getAppDatabasePath())*.db")
logger.debug("SimpleXApp init: legacy DB\(legacyDatabase ? "" : " not") present")
}
}
@@ -118,22 +115,14 @@ struct SimpleXApp: App {
}
private func authenticationExpired() -> Bool {
if let enteredBackground = enteredBackground {
if let enteredBackgroundAuthenticated = enteredBackgroundAuthenticated {
let delay = Double(UserDefaults.standard.integer(forKey: DEFAULT_LA_LOCK_DELAY))
return ProcessInfo.processInfo.systemUptime - enteredBackground >= delay
return ProcessInfo.processInfo.systemUptime - enteredBackgroundAuthenticated >= delay
} else {
return true
}
}
private func unlockedRecently() -> Bool {
if let lastSuccessfulUnlock = lastSuccessfulUnlock {
return ProcessInfo.processInfo.systemUptime - lastSuccessfulUnlock < 2
} else {
return false
}
}
private func updateChats() {
do {
let chats = try apiGetChats()

View File

@@ -38,21 +38,21 @@ struct ActiveCallView: View {
}
}
.onAppear {
logger.debug("ActiveCallView: appear client is nil \(client == nil), scenePhase \(String(describing: scenePhase), privacy: .public), canConnectCall \(canConnectCall)")
logger.debug("ActiveCallView: appear client is nil \(client == nil), scenePhase \(String(describing: scenePhase)), canConnectCall \(canConnectCall)")
AppDelegate.keepScreenOn(true)
createWebRTCClient()
dismissAllSheets()
}
.onChange(of: canConnectCall) { _ in
logger.debug("ActiveCallView: canConnectCall changed to \(canConnectCall, privacy: .public)")
logger.debug("ActiveCallView: canConnectCall changed to \(canConnectCall)")
createWebRTCClient()
}
.onDisappear {
logger.debug("ActiveCallView: disappear")
Task { await m.callCommand.setClient(nil) }
AppDelegate.keepScreenOn(false)
client?.endCall()
}
.onChange(of: m.callCommand) { _ in sendCommandToClient()}
.background(.black)
.preferredColorScheme(.dark)
}
@@ -60,19 +60,8 @@ struct ActiveCallView: View {
private func createWebRTCClient() {
if client == nil && canConnectCall {
client = WebRTCClient($activeCall, { msg in await MainActor.run { processRtcMessage(msg: msg) } }, $localRendererAspectRatio)
sendCommandToClient()
}
}
private func sendCommandToClient() {
if call == m.activeCall,
m.activeCall != nil,
let client = client,
let cmd = m.callCommand {
m.callCommand = nil
logger.debug("sendCallCommand: \(cmd.cmdType)")
Task {
await client.sendCallCommand(command: cmd)
await m.callCommand.setClient(client)
}
}
}
@@ -168,8 +157,10 @@ struct ActiveCallView: View {
}
case let .error(message):
logger.debug("ActiveCallView: command error: \(message)")
AlertManager.shared.showAlert(Alert(title: Text("Error"), message: Text(message)))
case let .invalid(type):
logger.debug("ActiveCallView: invalid response: \(type)")
AlertManager.shared.showAlert(Alert(title: Text("Invalid response"), message: Text(type)))
}
}
}
@@ -255,7 +246,6 @@ struct ActiveCallOverlay: View {
HStack {
Text(call.encryptionStatus)
if let connInfo = call.connectionInfo {
// Text("(") + Text(connInfo.text) + Text(", \(connInfo.protocolText))")
Text("(") + Text(connInfo.text) + Text(")")
}
}

View File

@@ -130,7 +130,7 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse
// The delay allows to accept the second call before suspending a chat
// see `.onChange(of: scenePhase)` in SimpleXApp
DispatchQueue.main.asyncAfter(deadline: .now() + 3) { [weak self] in
logger.debug("CallController: shouldSuspendChat \(String(describing: self?.shouldSuspendChat), privacy: .public)")
logger.debug("CallController: shouldSuspendChat \(String(describing: self?.shouldSuspendChat))")
if ChatModel.shared.activeCall == nil && self?.shouldSuspendChat == true {
self?.shouldSuspendChat = false
suspendChat()
@@ -142,33 +142,46 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse
@objc(pushRegistry:didUpdatePushCredentials:forType:)
func pushRegistry(_ registry: PKPushRegistry, didUpdate pushCredentials: PKPushCredentials, for type: PKPushType) {
logger.debug("CallController: didUpdate push credentials for type \(type.rawValue, privacy: .public)")
logger.debug("CallController: didUpdate push credentials for type \(type.rawValue)")
}
func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType, completion: @escaping () -> Void) {
logger.debug("CallController: did receive push with type \(type.rawValue, privacy: .public)")
logger.debug("CallController: did receive push with type \(type.rawValue)")
if type != .voIP {
completion()
return
}
logger.debug("CallController: initializing chat")
if (!ChatModel.shared.chatInitialized) {
initChatAndMigrate(refreshInvitations: false)
if AppChatState.shared.value == .stopped {
self.reportExpiredCall(payload: payload, completion)
return
}
startChatAndActivate()
shouldSuspendChat = true
if (!ChatModel.shared.chatInitialized) {
logger.debug("CallController: initializing chat")
do {
try initializeChat(start: true, refreshInvitations: false)
} catch let error {
logger.error("CallController: initializing chat error: \(error)")
self.reportExpiredCall(payload: payload, completion)
return
}
}
logger.debug("CallController: initialized chat")
startChatForCall()
logger.debug("CallController: started chat")
self.shouldSuspendChat = true
// There are no invitations in the model, as it was processed by NSE
_ = try? justRefreshCallInvitations()
logger.debug("CallController: updated call invitations chat")
// logger.debug("CallController justRefreshCallInvitations: \(String(describing: m.callInvitations))")
// Extract the call information from the push notification payload
let m = ChatModel.shared
if let contactId = payload.dictionaryPayload["contactId"] as? String,
let invitation = m.callInvitations[contactId] {
let update = cxCallUpdate(invitation: invitation)
let update = self.cxCallUpdate(invitation: invitation)
if let uuid = invitation.callkitUUID {
logger.debug("CallController: report pushkit call via CallKit")
let update = cxCallUpdate(invitation: invitation)
provider.reportNewIncomingCall(with: uuid, update: update) { error in
let update = self.cxCallUpdate(invitation: invitation)
self.provider.reportNewIncomingCall(with: uuid, update: update) { error in
if error != nil {
m.callInvitations.removeValue(forKey: contactId)
}
@@ -176,10 +189,10 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse
completion()
}
} else {
reportExpiredCall(update: update, completion)
self.reportExpiredCall(update: update, completion)
}
} else {
reportExpiredCall(payload: payload, completion)
self.reportExpiredCall(payload: payload, completion)
}
}
@@ -210,7 +223,7 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse
}
func reportNewIncomingCall(invitation: RcvCallInvitation, completion: @escaping (Error?) -> Void) {
logger.debug("CallController.reportNewIncomingCall, UUID=\(String(describing: invitation.callkitUUID), privacy: .public)")
logger.debug("CallController.reportNewIncomingCall, UUID=\(String(describing: invitation.callkitUUID))")
if CallController.useCallKit(), let uuid = invitation.callkitUUID {
if invitation.callTs.timeIntervalSinceNow >= -180 {
let update = cxCallUpdate(invitation: invitation)
@@ -350,7 +363,7 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse
private func requestTransaction(with action: CXAction, onSuccess: @escaping () -> Void = {}) {
controller.request(CXTransaction(action: action)) { error in
if let error = error {
logger.error("CallController.requestTransaction error requesting transaction: \(error.localizedDescription, privacy: .public)")
logger.error("CallController.requestTransaction error requesting transaction: \(error.localizedDescription)")
} else {
logger.debug("CallController.requestTransaction requested transaction successfully")
onSuccess()

View File

@@ -22,7 +22,7 @@ class CallManager {
let m = ChatModel.shared
if let call = m.activeCall, call.callkitUUID == callUUID {
m.showCallView = true
m.callCommand = .capabilities(media: call.localMedia)
Task { await m.callCommand.processCommand(.capabilities(media: call.localMedia)) }
return true
}
return false
@@ -57,19 +57,21 @@ class CallManager {
m.activeCall = call
m.showCallView = true
m.callCommand = .start(
Task {
await m.callCommand.processCommand(.start(
media: invitation.callType.media,
aesKey: invitation.sharedKey,
iceServers: iceServers,
relay: useRelay
)
))
}
}
}
func enableMedia(media: CallMediaType, enable: Bool, callUUID: UUID) -> Bool {
if let call = ChatModel.shared.activeCall, call.callkitUUID == callUUID {
let m = ChatModel.shared
m.callCommand = .media(media: media, enable: enable)
Task { await m.callCommand.processCommand(.media(media: media, enable: enable)) }
return true
}
return false
@@ -94,11 +96,13 @@ class CallManager {
completed()
} else {
logger.debug("CallManager.endCall: ending call...")
m.callCommand = .end
m.activeCall = nil
m.showCallView = false
completed()
Task {
await m.callCommand.processCommand(.end)
await MainActor.run {
m.activeCall = nil
m.showCallView = false
completed()
}
do {
try await apiEndCall(call.contact)
} catch {

View File

@@ -335,6 +335,50 @@ extension WCallResponse: Encodable {
}
}
actor WebRTCCommandProcessor {
private var client: WebRTCClient? = nil
private var commands: [WCallCommand] = []
private var running: Bool = false
func setClient(_ client: WebRTCClient?) async {
logger.debug("WebRTC: setClient, commands count \(self.commands.count)")
self.client = client
if client != nil {
await processAllCommands()
} else {
commands.removeAll()
}
}
func processCommand(_ c: WCallCommand) async {
// logger.debug("WebRTC: process command \(c.cmdType)")
commands.append(c)
if !running && client != nil {
await processAllCommands()
}
}
func processAllCommands() async {
logger.debug("WebRTC: process all commands, commands count \(self.commands.count), client == nil \(self.client == nil)")
if let client = client {
running = true
while let c = commands.first, shouldRunCommand(client, c) {
commands.remove(at: 0)
await client.sendCallCommand(command: c)
logger.debug("WebRTC: processed cmd \(c.cmdType)")
}
running = false
}
}
func shouldRunCommand(_ client: WebRTCClient, _ c: WCallCommand) -> Bool {
switch c {
case .capabilities, .start, .offer, .end: true
default: client.activeCall.wrappedValue != nil
}
}
}
struct ConnectionState: Codable, Equatable {
var connectionState: String
var iceConnectionState: String
@@ -358,26 +402,12 @@ struct ConnectionInfo: Codable, Equatable {
return "\(local?.rawValue ?? unknown) / \(remote?.rawValue ?? unknown)"
}
}
var protocolText: String {
let unknown = NSLocalizedString("unknown", comment: "connection info")
let local = localCandidate?.protocol?.uppercased() ?? unknown
let localRelay = localCandidate?.relayProtocol?.uppercased() ?? unknown
let remote = remoteCandidate?.protocol?.uppercased() ?? unknown
let localText = localRelay == local || localCandidate?.relayProtocol == nil
? local
: "\(local) (\(localRelay))"
return local == remote
? localText
: "\(localText) / \(remote)"
}
}
// https://developer.mozilla.org/en-US/docs/Web/API/RTCIceCandidate
struct RTCIceCandidate: Codable, Equatable {
var candidateType: RTCIceCandidateType?
var `protocol`: String?
var relayProtocol: String?
var sdpMid: String?
var sdpMLineIndex: Int?
var candidate: String

View File

@@ -18,10 +18,11 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
}()
private static let ivTagBytes: Int = 28
private static let enableEncryption: Bool = true
private var chat_ctrl = getChatCtrl()
struct Call {
var connection: RTCPeerConnection
var iceCandidates: [RTCIceCandidate]
var iceCandidates: IceCandidates
var localMedia: CallMediaType
var localCamera: RTCVideoCapturer?
var localVideoSource: RTCVideoSource?
@@ -33,10 +34,24 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
var frameDecryptor: RTCFrameDecryptor?
}
actor IceCandidates {
private var candidates: [RTCIceCandidate] = []
func getAndClear() async -> [RTCIceCandidate] {
let cs = candidates
candidates = []
return cs
}
func append(_ c: RTCIceCandidate) async {
candidates.append(c)
}
}
private let rtcAudioSession = RTCAudioSession.sharedInstance()
private let audioQueue = DispatchQueue(label: "audio")
private var sendCallResponse: (WVAPIMessage) async -> Void
private var activeCall: Binding<Call?>
var activeCall: Binding<Call?>
private var localRendererAspectRatio: Binding<CGFloat?>
@available(*, unavailable)
@@ -60,7 +75,7 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
WebRTC.RTCIceServer(urlStrings: ["turn:turn.simplex.im:443?transport=tcp"], username: "private", credential: "yleob6AVkiNI87hpR94Z"),
]
func initializeCall(_ iceServers: [WebRTC.RTCIceServer]?, _ remoteIceCandidates: [RTCIceCandidate], _ mediaType: CallMediaType, _ aesKey: String?, _ relay: Bool?) -> Call {
func initializeCall(_ iceServers: [WebRTC.RTCIceServer]?, _ mediaType: CallMediaType, _ aesKey: String?, _ relay: Bool?) -> Call {
let connection = createPeerConnection(iceServers ?? getWebRTCIceServers() ?? defaultIceServers, relay)
connection.delegate = self
createAudioSender(connection)
@@ -87,7 +102,7 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
}
return Call(
connection: connection,
iceCandidates: remoteIceCandidates,
iceCandidates: IceCandidates(),
localMedia: mediaType,
localCamera: localCamera,
localVideoSource: localVideoSource,
@@ -144,26 +159,18 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
logger.debug("starting incoming call - create webrtc session")
if activeCall.wrappedValue != nil { endCall() }
let encryption = WebRTCClient.enableEncryption
let call = initializeCall(iceServers?.toWebRTCIceServers(), [], media, encryption ? aesKey : nil, relay)
let call = initializeCall(iceServers?.toWebRTCIceServers(), media, encryption ? aesKey : nil, relay)
activeCall.wrappedValue = call
call.connection.offer { answer in
Task {
let gotCandidates = await self.waitWithTimeout(10_000, stepMs: 1000, until: { self.activeCall.wrappedValue?.iceCandidates.count ?? 0 > 0 })
if gotCandidates {
await self.sendCallResponse(.init(
corrId: nil,
resp: .offer(
offer: compressToBase64(input: encodeJSON(CustomRTCSessionDescription(type: answer.type.toSdpType(), sdp: answer.sdp))),
iceCandidates: compressToBase64(input: encodeJSON(self.activeCall.wrappedValue?.iceCandidates ?? [])),
capabilities: CallCapabilities(encryption: encryption)
),
command: command)
)
} else {
self.endCall()
}
}
let (offer, error) = await call.connection.offer()
if let offer = offer {
resp = .offer(
offer: compressToBase64(input: encodeJSON(CustomRTCSessionDescription(type: offer.type.toSdpType(), sdp: offer.sdp))),
iceCandidates: compressToBase64(input: encodeJSON(await self.getInitialIceCandidates())),
capabilities: CallCapabilities(encryption: encryption)
)
self.waitForMoreIceCandidates()
} else {
resp = .error(message: "offer error: \(error?.localizedDescription ?? "unknown error")")
}
case let .offer(offer, iceCandidates, media, aesKey, iceServers, relay):
if activeCall.wrappedValue != nil {
@@ -172,26 +179,21 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
resp = .error(message: "accept: encryption is not supported")
} else if let offer: CustomRTCSessionDescription = decodeJSON(decompressFromBase64(input: offer)),
let remoteIceCandidates: [RTCIceCandidate] = decodeJSON(decompressFromBase64(input: iceCandidates)) {
let call = initializeCall(iceServers?.toWebRTCIceServers(), remoteIceCandidates, media, WebRTCClient.enableEncryption ? aesKey : nil, relay)
let call = initializeCall(iceServers?.toWebRTCIceServers(), media, WebRTCClient.enableEncryption ? aesKey : nil, relay)
activeCall.wrappedValue = call
let pc = call.connection
if let type = offer.type, let sdp = offer.sdp {
if (try? await pc.setRemoteDescription(RTCSessionDescription(type: type.toWebRTCSdpType(), sdp: sdp))) != nil {
pc.answer { answer in
let (answer, error) = await pc.answer()
if let answer = answer {
self.addIceCandidates(pc, remoteIceCandidates)
// Task {
// try? await Task.sleep(nanoseconds: 32_000 * 1000000)
Task {
await self.sendCallResponse(.init(
corrId: nil,
resp: .answer(
answer: compressToBase64(input: encodeJSON(CustomRTCSessionDescription(type: answer.type.toSdpType(), sdp: answer.sdp))),
iceCandidates: compressToBase64(input: encodeJSON(call.iceCandidates))
),
command: command)
)
}
// }
resp = .answer(
answer: compressToBase64(input: encodeJSON(CustomRTCSessionDescription(type: answer.type.toSdpType(), sdp: answer.sdp))),
iceCandidates: compressToBase64(input: encodeJSON(await self.getInitialIceCandidates()))
)
self.waitForMoreIceCandidates()
} else {
resp = .error(message: "answer error: \(error?.localizedDescription ?? "unknown error")")
}
} else {
resp = .error(message: "accept: remote description is not set")
@@ -234,6 +236,7 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
resp = .ok
}
case .end:
// TODO possibly, endCall should be called before returning .ok
await sendCallResponse(.init(corrId: nil, resp: .ok, command: command))
endCall()
}
@@ -242,6 +245,33 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
}
}
func getInitialIceCandidates() async -> [RTCIceCandidate] {
await untilIceComplete(timeoutMs: 750, stepMs: 150) {}
let candidates = await activeCall.wrappedValue?.iceCandidates.getAndClear() ?? []
logger.debug("WebRTCClient: sending initial ice candidates: \(candidates.count)")
return candidates
}
func waitForMoreIceCandidates() {
Task {
await untilIceComplete(timeoutMs: 12000, stepMs: 1500) {
let candidates = await self.activeCall.wrappedValue?.iceCandidates.getAndClear() ?? []
if candidates.count > 0 {
logger.debug("WebRTCClient: sending more ice candidates: \(candidates.count)")
await self.sendIceCandidates(candidates)
}
}
}
}
func sendIceCandidates(_ candidates: [RTCIceCandidate]) async {
await self.sendCallResponse(.init(
corrId: nil,
resp: .ice(iceCandidates: compressToBase64(input: encodeJSON(candidates))),
command: nil)
)
}
func enableMedia(_ media: CallMediaType, _ enable: Bool) {
logger.debug("WebRTCClient: enabling media \(media.rawValue) \(enable)")
media == .video ? setVideoEnabled(enable) : setAudioEnabled(enable)
@@ -279,7 +309,7 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
memcpy(pointer, (unencrypted as NSData).bytes, unencrypted.count)
let isKeyFrame = unencrypted[0] & 1 == 0
let clearTextBytesSize = mediaType.rawValue == 0 ? 1 : isKeyFrame ? 10 : 3
logCrypto("encrypt", chat_encrypt_media(&key, pointer.advanced(by: clearTextBytesSize), Int32(unencrypted.count + WebRTCClient.ivTagBytes - clearTextBytesSize)))
logCrypto("encrypt", chat_encrypt_media(chat_ctrl, &key, pointer.advanced(by: clearTextBytesSize), Int32(unencrypted.count + WebRTCClient.ivTagBytes - clearTextBytesSize)))
return Data(bytes: pointer, count: unencrypted.count + WebRTCClient.ivTagBytes)
} else {
return nil
@@ -387,12 +417,13 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
audioSessionToDefaults()
}
func waitWithTimeout(_ timeoutMs: UInt64, stepMs: UInt64, until success: () -> Bool) async -> Bool {
let startedAt = DispatchTime.now()
while !success() && startedAt.uptimeNanoseconds + timeoutMs * 1000000 > DispatchTime.now().uptimeNanoseconds {
guard let _ = try? await Task.sleep(nanoseconds: stepMs * 1000000) else { break }
}
return success()
func untilIceComplete(timeoutMs: UInt64, stepMs: UInt64, action: @escaping () async -> Void) async {
var t: UInt64 = 0
repeat {
_ = try? await Task.sleep(nanoseconds: stepMs * 1000000)
t += stepMs
await action()
} while t < timeoutMs && activeCall.wrappedValue?.connection.iceGatheringState != .complete
}
}
@@ -405,25 +436,33 @@ extension WebRTC.RTCPeerConnection {
optionalConstraints: nil)
}
func offer(_ completion: @escaping (_ sdp: RTCSessionDescription) -> Void) {
offer(for: mediaConstraints()) { (sdp, error) in
guard let sdp = sdp else {
return
func offer() async -> (RTCSessionDescription?, Error?) {
await withCheckedContinuation { cont in
offer(for: mediaConstraints()) { (sdp, error) in
self.processSDP(cont, sdp, error)
}
self.setLocalDescription(sdp, completionHandler: { (error) in
completion(sdp)
})
}
}
func answer(_ completion: @escaping (_ sdp: RTCSessionDescription) -> Void) {
answer(for: mediaConstraints()) { (sdp, error) in
guard let sdp = sdp else {
return
func answer() async -> (RTCSessionDescription?, Error?) {
await withCheckedContinuation { cont in
answer(for: mediaConstraints()) { (sdp, error) in
self.processSDP(cont, sdp, error)
}
}
}
private func processSDP(_ cont: CheckedContinuation<(RTCSessionDescription?, Error?), Never>, _ sdp: RTCSessionDescription?, _ error: Error?) {
if let sdp = sdp {
self.setLocalDescription(sdp, completionHandler: { (error) in
completion(sdp)
if let error = error {
cont.resume(returning: (nil, error))
} else {
cont.resume(returning: (sdp, nil))
}
})
} else {
cont.resume(returning: (nil, error))
}
}
}
@@ -479,6 +518,7 @@ extension WebRTCClient: RTCPeerConnectionDelegate {
default: enableSpeaker = false
}
setSpeakerEnabledAndConfigureSession(enableSpeaker)
case .connected: sendConnectedEvent(connection)
case .disconnected, .failed: endCall()
default: do {}
}
@@ -491,7 +531,9 @@ extension WebRTCClient: RTCPeerConnectionDelegate {
func peerConnection(_ connection: RTCPeerConnection, didGenerate candidate: WebRTC.RTCIceCandidate) {
// logger.debug("Connection generated candidate \(candidate.debugDescription)")
activeCall.wrappedValue?.iceCandidates.append(candidate.toCandidate(nil, nil, nil))
Task {
await self.activeCall.wrappedValue?.iceCandidates.append(candidate.toCandidate(nil, nil))
}
}
func peerConnection(_ connection: RTCPeerConnection, didRemove candidates: [WebRTC.RTCIceCandidate]) {
@@ -506,10 +548,9 @@ extension WebRTCClient: RTCPeerConnectionDelegate {
lastReceivedMs lastDataReceivedMs: Int32,
changeReason reason: String) {
// logger.debug("Connection changed candidate \(reason) \(remote.debugDescription) \(remote.description)")
sendConnectedEvent(connection, local: local, remote: remote)
}
func sendConnectedEvent(_ connection: WebRTC.RTCPeerConnection, local: WebRTC.RTCIceCandidate, remote: WebRTC.RTCIceCandidate) {
func sendConnectedEvent(_ connection: WebRTC.RTCPeerConnection) {
connection.statistics { (stats: RTCStatisticsReport) in
stats.statistics.values.forEach { stat in
// logger.debug("Stat \(stat.debugDescription)")
@@ -517,24 +558,25 @@ extension WebRTCClient: RTCPeerConnectionDelegate {
let localId = stat.values["localCandidateId"] as? String,
let remoteId = stat.values["remoteCandidateId"] as? String,
let localStats = stats.statistics[localId],
let remoteStats = stats.statistics[remoteId],
local.sdp.contains("\((localStats.values["ip"] as? String ?? "--")) \((localStats.values["port"] as? String ?? "--"))") &&
remote.sdp.contains("\((remoteStats.values["ip"] as? String ?? "--")) \((remoteStats.values["port"] as? String ?? "--"))")
let remoteStats = stats.statistics[remoteId]
{
Task {
await self.sendCallResponse(.init(
corrId: nil,
resp: .connected(connectionInfo: ConnectionInfo(
localCandidate: local.toCandidate(
RTCIceCandidateType.init(rawValue: localStats.values["candidateType"] as! String),
localStats.values["protocol"] as? String,
localStats.values["relayProtocol"] as? String
localCandidate: RTCIceCandidate(
candidateType: RTCIceCandidateType.init(rawValue: localStats.values["candidateType"] as! String),
protocol: localStats.values["protocol"] as? String,
sdpMid: nil,
sdpMLineIndex: nil,
candidate: ""
),
remoteCandidate: remote.toCandidate(
RTCIceCandidateType.init(rawValue: remoteStats.values["candidateType"] as! String),
remoteStats.values["protocol"] as? String,
remoteStats.values["relayProtocol"] as? String
))),
remoteCandidate: RTCIceCandidate(
candidateType: RTCIceCandidateType.init(rawValue: remoteStats.values["candidateType"] as! String),
protocol: remoteStats.values["protocol"] as? String,
sdpMid: nil,
sdpMLineIndex: nil,
candidate: ""))),
command: nil)
)
}
@@ -634,11 +676,10 @@ extension RTCIceCandidate {
}
extension WebRTC.RTCIceCandidate {
func toCandidate(_ candidateType: RTCIceCandidateType?, _ protocol: String?, _ relayProtocol: String?) -> RTCIceCandidate {
func toCandidate(_ candidateType: RTCIceCandidateType?, _ protocol: String?) -> RTCIceCandidate {
RTCIceCandidate(
candidateType: candidateType,
protocol: `protocol`,
relayProtocol: relayProtocol,
sdpMid: sdpMid,
sdpMLineIndex: Int(sdpMLineIndex),
candidate: sdp

View File

@@ -52,7 +52,7 @@ struct CIFileView: View {
private var itemInteractive: Bool {
if let file = file {
switch (file.fileStatus) {
case .sndStored: return false
case .sndStored: return file.fileProtocol == .local
case .sndTransfer: return false
case .sndComplete: return false
case .sndCancelled: return false
@@ -85,8 +85,7 @@ struct CIFileView: View {
Task {
logger.debug("CIFileView fileAction - in .rcvInvitation, in Task")
if let user = m.currentUser {
let encrypted = privacyEncryptLocalFilesGroupDefault.get()
await receiveFile(user: user, fileId: file.fileId, encrypted: encrypted)
await receiveFile(user: user, fileId: file.fileId)
}
}
} else {
@@ -108,12 +107,18 @@ struct CIFileView: View {
title: "Waiting for file",
message: "File will be received when your contact is online, please wait or check later!"
)
case .local: ()
}
case .rcvComplete:
logger.debug("CIFileView fileAction - in .rcvComplete")
if let fileSource = getLoadedFileSource(file) {
saveCryptoFile(fileSource)
}
case .sndStored:
logger.debug("CIFileView fileAction - in .sndStored")
if file.fileProtocol == .local, let fileSource = getLoadedFileSource(file) {
saveCryptoFile(fileSource)
}
default: break
}
}
@@ -126,11 +131,13 @@ struct CIFileView: View {
switch file.fileProtocol {
case .xftp: progressView()
case .smp: fileIcon("doc.fill")
case .local: fileIcon("doc.fill")
}
case let .sndTransfer(sndProgress, sndTotal):
switch file.fileProtocol {
case .xftp: progressCircle(sndProgress, sndTotal)
case .smp: progressView()
case .local: EmptyView()
}
case .sndComplete: fileIcon("doc.fill", innerIcon: "checkmark", innerIconSize: 10)
case .sndCancelled: fileIcon("doc.fill", innerIcon: "xmark", innerIconSize: 10)

View File

@@ -38,7 +38,7 @@ struct CIImageView: View {
case .rcvInvitation:
Task {
if let user = m.currentUser {
await receiveFile(user: user, fileId: file.fileId, encrypted: chatItem.encryptLocalFile)
await receiveFile(user: user, fileId: file.fileId)
}
}
case .rcvAccepted:
@@ -53,6 +53,7 @@ struct CIImageView: View {
title: "Waiting for image",
message: "Image will be received when your contact is online, please wait or check later!"
)
case .local: ()
}
case .rcvTransfer: () // ?
case .rcvComplete: () // ?
@@ -90,6 +91,7 @@ struct CIImageView: View {
switch file.fileProtocol {
case .xftp: progressView()
case .smp: EmptyView()
case .local: EmptyView()
}
case .sndTransfer: progressView()
case .sndComplete: fileIcon("checkmark", 10, 13)

View File

@@ -26,6 +26,8 @@ struct CIVideoView: View {
@State private var player: AVPlayer?
@State private var fullPlayer: AVPlayer?
@State private var url: URL?
@State private var urlDecrypted: URL?
@State private var decryptionInProgress: Bool = false
@State private var showFullScreenPlayer = false
@State private var timeObserver: Any? = nil
@State private var fullScreenTimeObserver: Any? = nil
@@ -39,8 +41,12 @@ struct CIVideoView: View {
self._videoWidth = videoWidth
self.scrollProxy = scrollProxy
if let url = getLoadedVideo(chatItem.file) {
self._player = State(initialValue: VideoPlayerView.getOrCreatePlayer(url, false))
self._fullPlayer = State(initialValue: AVPlayer(url: url))
let decrypted = chatItem.file?.fileSource?.cryptoArgs == nil ? url : chatItem.file?.fileSource?.decryptedGet()
self._urlDecrypted = State(initialValue: decrypted)
if let decrypted = decrypted {
self._player = State(initialValue: VideoPlayerView.getOrCreatePlayer(decrypted, false))
self._fullPlayer = State(initialValue: AVPlayer(url: decrypted))
}
self._url = State(initialValue: url)
}
if let data = Data(base64Encoded: dropImagePrefix(image)),
@@ -53,8 +59,10 @@ struct CIVideoView: View {
let file = chatItem.file
ZStack {
ZStack(alignment: .topLeading) {
if let file = file, let preview = preview, let player = player, let url = url {
videoView(player, url, file, preview, duration)
if let file = file, let preview = preview, let player = player, let decrypted = urlDecrypted {
videoView(player, decrypted, file, preview, duration)
} else if let file = file, let defaultPreview = preview, file.loaded && urlDecrypted == nil {
videoViewEncrypted(file, defaultPreview, duration)
} else if let data = Data(base64Encoded: dropImagePrefix(image)),
let uiImage = UIImage(data: data) {
imageView(uiImage)
@@ -62,7 +70,7 @@ struct CIVideoView: View {
if let file = file {
switch file.fileStatus {
case .rcvInvitation:
receiveFileIfValidSize(file: file, encrypted: false, receiveFile: receiveFile)
receiveFileIfValidSize(file: file, receiveFile: receiveFile)
case .rcvAccepted:
switch file.fileProtocol {
case .xftp:
@@ -75,6 +83,7 @@ struct CIVideoView: View {
title: "Waiting for video",
message: "Video will be received when your contact is online, please wait or check later!"
)
case .local: ()
}
case .rcvTransfer: () // ?
case .rcvComplete: () // ?
@@ -88,7 +97,7 @@ struct CIVideoView: View {
}
if let file = file, case .rcvInvitation = file.fileStatus {
Button {
receiveFileIfValidSize(file: file, encrypted: false, receiveFile: receiveFile)
receiveFileIfValidSize(file: file, receiveFile: receiveFile)
} label: {
playPauseIcon("play.fill")
}
@@ -96,12 +105,46 @@ struct CIVideoView: View {
}
}
private func videoViewEncrypted(_ file: CIFile, _ defaultPreview: UIImage, _ duration: Int) -> some View {
return ZStack(alignment: .topTrailing) {
ZStack(alignment: .center) {
let canBePlayed = !chatItem.chatDir.sent || file.fileStatus == CIFileStatus.sndComplete || (file.fileStatus == .sndStored && file.fileProtocol == .local)
imageView(defaultPreview)
.fullScreenCover(isPresented: $showFullScreenPlayer) {
if let decrypted = urlDecrypted {
fullScreenPlayer(decrypted)
}
}
.onTapGesture {
decrypt(file: file) {
showFullScreenPlayer = urlDecrypted != nil
}
}
if !decryptionInProgress {
Button {
decrypt(file: file) {
if let decrypted = urlDecrypted {
videoPlaying = true
player?.play()
}
}
} label: {
playPauseIcon(canBePlayed ? "play.fill" : "play.slash")
}
.disabled(!canBePlayed)
} else {
videoDecryptionProgress()
}
}
}
}
private func videoView(_ player: AVPlayer, _ url: URL, _ file: CIFile, _ preview: UIImage, _ duration: Int) -> some View {
let w = preview.size.width <= preview.size.height ? maxWidth * 0.75 : maxWidth
DispatchQueue.main.async { videoWidth = w }
return ZStack(alignment: .topTrailing) {
ZStack(alignment: .center) {
let canBePlayed = !chatItem.chatDir.sent || file.fileStatus == CIFileStatus.sndComplete
let canBePlayed = !chatItem.chatDir.sent || file.fileStatus == CIFileStatus.sndComplete || (file.fileStatus == .sndStored && file.fileProtocol == .local)
VideoPlayerView(player: player, url: url, showControls: false)
.frame(width: w, height: w * preview.size.height / preview.size.width)
.onChange(of: m.stopPreviousRecPlay) { playingUrl in
@@ -159,6 +202,16 @@ struct CIVideoView: View {
.clipShape(Circle())
}
private func videoDecryptionProgress(_ color: Color = .white) -> some View {
ProgressView()
.progressViewStyle(.circular)
.frame(width: 12, height: 12)
.tint(color)
.frame(width: 40, height: 40)
.background(Color.black.opacity(0.35))
.clipShape(Circle())
}
private func durationProgress() -> some View {
HStack {
Text("\(durationText(videoPlaying ? progress : duration))")
@@ -202,11 +255,13 @@ struct CIVideoView: View {
switch file.fileProtocol {
case .xftp: progressView()
case .smp: EmptyView()
case .local: EmptyView()
}
case let .sndTransfer(sndProgress, sndTotal):
switch file.fileProtocol {
case .xftp: progressCircle(sndProgress, sndTotal)
case .smp: progressView()
case .local: EmptyView()
}
case .sndComplete: fileIcon("checkmark", 10, 13)
case .sndCancelled: fileIcon("xmark", 10, 13)
@@ -257,10 +312,10 @@ struct CIVideoView: View {
}
// TODO encrypt: where file size is checked?
private func receiveFileIfValidSize(file: CIFile, encrypted: Bool, receiveFile: @escaping (User, Int64, Bool, Bool) async -> Void) {
private func receiveFileIfValidSize(file: CIFile, receiveFile: @escaping (User, Int64, Bool) async -> Void) {
Task {
if let user = m.currentUser {
await receiveFile(user, file.fileId, encrypted, false)
await receiveFile(user, file.fileId, false)
}
}
}
@@ -323,6 +378,22 @@ struct CIVideoView: View {
}
}
private func decrypt(file: CIFile, completed: (() -> Void)? = nil) {
if decryptionInProgress { return }
decryptionInProgress = true
Task {
urlDecrypted = await file.fileSource?.decryptedGetOrCreate(&ChatModel.shared.filesToDelete)
await MainActor.run {
if let decrypted = urlDecrypted {
player = VideoPlayerView.getOrCreatePlayer(decrypted, false)
fullPlayer = AVPlayer(url: decrypted)
}
decryptionInProgress = true
completed?()
}
}
}
private func addObserver(_ player: AVPlayer, _ url: URL) {
timeObserver = player.addPeriodicTimeObserver(forInterval: CMTime(seconds: 0.01, preferredTimescale: CMTimeScale(NSEC_PER_SEC)), queue: .main) { time in
if let item = player.currentItem {

View File

@@ -221,7 +221,7 @@ struct VoiceMessagePlayer: View {
Button {
Task {
if let user = chatModel.currentUser {
await receiveFile(user: user, fileId: recordingFile.fileId, encrypted: privacyEncryptLocalFilesGroupDefault.get())
await receiveFile(user: user, fileId: recordingFile.fileId)
}
}
} label: {

View File

@@ -9,6 +9,8 @@
import SwiftUI
import SimpleXChat
let notesChatColorLight = Color(.sRGB, red: 0.27, green: 0.72, blue: 1, opacity: 0.21)
let notesChatColorDark = Color(.sRGB, red: 0.27, green: 0.72, blue: 1, opacity: 0.19)
let sentColorLight = Color(.sRGB, red: 0.27, green: 0.72, blue: 1, opacity: 0.12)
let sentColorDark = Color(.sRGB, red: 0.27, green: 0.72, blue: 1, opacity: 0.17)
private let sentQuoteColorLight = Color(.sRGB, red: 0.27, green: 0.72, blue: 1, opacity: 0.11)
@@ -28,7 +30,9 @@ struct FramedItemView: View {
@State var metaColor = Color.secondary
@State var showFullScreenImage = false
@Binding var allowMenu: Bool
@State private var showSecrets = false
@State private var showQuoteSecrets = false
@Binding var audioPlayer: AudioPlayer?
@Binding var playbackState: VoiceMessagePlaybackState
@Binding var playbackTime: TimeInterval?
@@ -42,7 +46,9 @@ struct FramedItemView: View {
framedItemHeader(icon: "flag", caption: Text("moderated by \(byGroupMember.displayName)").italic())
case .blocked:
framedItemHeader(icon: "hand.raised", caption: Text("blocked").italic())
default:
case .blockedByAdmin:
framedItemHeader(icon: "hand.raised", caption: Text("blocked by admin").italic())
case .deleted:
framedItemHeader(icon: "trash", caption: Text("marked deleted").italic())
}
} else if chatItem.meta.isLive {
@@ -252,10 +258,12 @@ struct FramedItemView: View {
}
private func ciQuotedMsgTextView(_ qi: CIQuote, lines: Int) -> some View {
MsgContentView(chat: chat, text: qi.text, formattedText: qi.formattedText)
.lineLimit(lines)
.font(.subheadline)
.padding(.bottom, 6)
toggleSecrets(qi.formattedText, $showQuoteSecrets,
MsgContentView(chat: chat, text: qi.text, formattedText: qi.formattedText, showSecrets: showQuoteSecrets)
.lineLimit(lines)
.font(.subheadline)
.padding(.bottom, 6)
)
}
private func ciQuoteIconView(_ image: String) -> some View {
@@ -278,13 +286,15 @@ struct FramedItemView: View {
@ViewBuilder private func ciMsgContentView(_ ci: ChatItem) -> some View {
let text = ci.meta.isLive ? ci.content.msgContent?.text ?? ci.text : ci.text
let rtl = isRightToLeft(text)
let v = MsgContentView(
let ft = text == "" ? [] : ci.formattedText
let v = toggleSecrets(ft, $showSecrets, MsgContentView(
chat: chat,
text: text,
formattedText: text == "" ? [] : ci.formattedText,
formattedText: ft,
meta: ci.meta,
rightToLeft: rtl
)
rightToLeft: rtl,
showSecrets: showSecrets
))
.multilineTextAlignment(rtl ? .trailing : .leading)
.padding(.vertical, 6)
.padding(.horizontal, 12)
@@ -298,7 +308,7 @@ struct FramedItemView: View {
v
}
}
@ViewBuilder private func ciFileView(_ ci: ChatItem, _ text: String) -> some View {
CIFileView(file: chatItem.file, edited: chatItem.meta.itemEdited)
.overlay(DetermineWidth())
@@ -318,6 +328,14 @@ struct FramedItemView: View {
}
}
@ViewBuilder func toggleSecrets<V: View>(_ ft: [FormattedText]?, _ showSecrets: Binding<Bool>, _ v: V) -> some View {
if let ft = ft, ft.contains(where: { $0.isSecret }) {
v.onTapGesture { showSecrets.wrappedValue.toggle() }
} else {
v
}
}
func isRightToLeft(_ s: String) -> Bool {
if let lang = CFStringTokenizerCopyBestStringLanguage(s as CFString, CFRange(location: 0, length: min(s.count, 80))) {
return NSLocale.characterDirection(forLanguage: lang as String) == .rightToLeft

View File

@@ -33,6 +33,7 @@ struct MarkedDeletedItemView: View {
var i = m.getChatItemIndex(chatItem) {
var moderated = 0
var blocked = 0
var blockedByAdmin = 0
var deleted = 0
var moderatedBy: Set<String> = []
while i < m.reversedChatItems.count,
@@ -44,16 +45,19 @@ struct MarkedDeletedItemView: View {
moderated += 1
moderatedBy.insert(byGroupMember.displayName)
case .blocked: blocked += 1
case .blockedByAdmin: blockedByAdmin += 1
case .deleted: deleted += 1
}
i += 1
}
let total = moderated + blocked + deleted
let total = moderated + blocked + blockedByAdmin + deleted
return total <= 1
? markedDeletedText
: total == moderated
? "\(total) messages moderated by \(moderatedBy.joined(separator: ", "))"
: total == blocked
: total == blockedByAdmin
? "\(total) messages blocked by admin"
: total == blocked + blockedByAdmin
? "\(total) messages blocked"
: "\(total) messages marked deleted"
} else {
@@ -61,11 +65,14 @@ struct MarkedDeletedItemView: View {
}
}
// same texts are in markedDeletedText in ChatPreviewView, but it returns String;
// can be refactored into a single function if functions calling these are changed to return same type
var markedDeletedText: LocalizedStringKey {
switch chatItem.meta.itemDeleted {
case let .moderated(_, byGroupMember): "moderated by \(byGroupMember.displayName)"
case .blocked: "blocked"
default: "marked deleted"
case .blockedByAdmin: "blocked by admin"
case .deleted, nil: "marked deleted"
}
}
}

View File

@@ -9,7 +9,7 @@
import SwiftUI
import SimpleXChat
private let uiLinkColor = UIColor(red: 0, green: 0.533, blue: 1, alpha: 1)
let uiLinkColor = UIColor(red: 0, green: 0.533, blue: 1, alpha: 1)
private let noTyping = Text(" ")
@@ -31,6 +31,7 @@ struct MsgContentView: View {
var sender: String? = nil
var meta: CIMeta? = nil
var rightToLeft = false
var showSecrets: Bool
@State private var typingIdx = 0
@State private var timer: Timer?
@@ -62,7 +63,7 @@ struct MsgContentView: View {
}
private func msgContentView() -> Text {
var v = messageText(text, formattedText, sender)
var v = messageText(text, formattedText, sender, showSecrets: showSecrets)
if let mt = meta {
if mt.isLive {
v = v + typingIndicator(mt.recent)
@@ -84,14 +85,14 @@ struct MsgContentView: View {
}
}
func messageText(_ text: String, _ formattedText: [FormattedText]?, _ sender: String?, icon: String? = nil, preview: Bool = false) -> Text {
func messageText(_ text: String, _ formattedText: [FormattedText]?, _ sender: String?, icon: String? = nil, preview: Bool = false, showSecrets: Bool) -> Text {
let s = text
var res: Text
if let ft = formattedText, ft.count > 0 && ft.count <= 200 {
res = formatText(ft[0], preview)
res = formatText(ft[0], preview, showSecret: showSecrets)
var i = 1
while i < ft.count {
res = res + formatText(ft[i], preview)
res = res + formatText(ft[i], preview, showSecret: showSecrets)
i = i + 1
}
} else {
@@ -110,7 +111,7 @@ func messageText(_ text: String, _ formattedText: [FormattedText]?, _ sender: St
}
}
private func formatText(_ ft: FormattedText, _ preview: Bool) -> Text {
private func formatText(_ ft: FormattedText, _ preview: Bool, showSecret: Bool) -> Text {
let t = ft.text
if let f = ft.format {
switch (f) {
@@ -118,7 +119,13 @@ private func formatText(_ ft: FormattedText, _ preview: Bool) -> Text {
case .italic: return Text(t).italic()
case .strikeThrough: return Text(t).strikethrough()
case .snippet: return Text(t).font(.body.monospaced())
case .secret: return Text(t).foregroundColor(.clear).underline(color: .primary)
case .secret: return
showSecret
? Text(t)
: Text(AttributedString(t, attributes: AttributeContainer([
.foregroundColor: UIColor.clear as Any,
.backgroundColor: UIColor.secondarySystemFill as Any
])))
case let .colored(color): return Text(t).foregroundColor(color.uiColor)
case .uri: return linkText(t, t, preview, prefix: "")
case let .simplexLink(linkType, simplexUri, smpHosts):
@@ -144,7 +151,7 @@ private func linkText(_ s: String, _ link: String, _ preview: Bool, prefix: Stri
]))).underline()
}
private func simplexLinkText(_ linkType: SimplexLinkType, _ smpHosts: [String]) -> String {
func simplexLinkText(_ linkType: SimplexLinkType, _ smpHosts: [String]) -> String {
linkType.description + " " + "(via \(smpHosts.first ?? "?"))"
}
@@ -156,7 +163,8 @@ struct MsgContentView_Previews: PreviewProvider {
text: chatItem.text,
formattedText: chatItem.formattedText,
sender: chatItem.memberDisplayName,
meta: chatItem.meta
meta: chatItem.meta,
showSecrets: false
)
.environmentObject(Chat.sampleData)
}

View File

@@ -53,7 +53,9 @@ struct ChatItemInfoView: View {
}
private var title: String {
ci.chatDir.sent
ci.localNote
? NSLocalizedString("Saved message", comment: "message info title")
: ci.chatDir.sent
? NSLocalizedString("Sent message", comment: "message info title")
: NSLocalizedString("Received message", comment: "message info title")
}
@@ -110,7 +112,11 @@ struct ChatItemInfoView: View {
.bold()
.padding(.bottom)
infoRow("Sent at", localTimestamp(meta.itemTs))
if ci.localNote {
infoRow("Created at", localTimestamp(meta.itemTs))
} else {
infoRow("Sent at", localTimestamp(meta.itemTs))
}
if !ci.chatDir.sent {
infoRow("Received at", localTimestamp(meta.createdAt))
}
@@ -168,7 +174,6 @@ struct ChatItemInfoView: View {
@ViewBuilder private func itemVersionView(_ itemVersion: ChatItemVersion, _ maxWidth: CGFloat, current: Bool) -> some View {
VStack(alignment: .leading, spacing: 4) {
textBubble(itemVersion.msgContent.text, itemVersion.formattedText, nil)
.allowsHitTesting(false)
.padding(.horizontal, 12)
.padding(.vertical, 6)
.background(chatItemFrameColor(ci, colorScheme))
@@ -198,7 +203,7 @@ struct ChatItemInfoView: View {
@ViewBuilder private func textBubble(_ text: String, _ formattedText: [FormattedText]?, _ sender: String? = nil) -> some View {
if text != "" {
messageText(text, formattedText, sender)
TextBubble(text: text, formattedText: formattedText, sender: sender)
} else {
Text("no text")
.italic()
@@ -206,6 +211,17 @@ struct ChatItemInfoView: View {
}
}
private struct TextBubble: View {
var text: String
var formattedText: [FormattedText]?
var sender: String? = nil
@State private var showSecrets = false
var body: some View {
toggleSecrets(formattedText, $showSecrets, messageText(text, formattedText, sender, showSecrets: showSecrets))
}
}
@ViewBuilder private func quoteTab(_ qi: CIQuote) -> some View {
GeometryReader { g in
let maxWidth = (g.size.width - 32) * 0.84
@@ -227,7 +243,6 @@ struct ChatItemInfoView: View {
@ViewBuilder private func quotedMsgView(_ qi: CIQuote, _ maxWidth: CGFloat) -> some View {
VStack(alignment: .leading, spacing: 4) {
textBubble(qi.text, qi.formattedText, qi.getSender(nil))
.allowsHitTesting(false)
.padding(.horizontal, 12)
.padding(.vertical, 6)
.background(quotedMsgFrameColor(qi, colorScheme))
@@ -341,7 +356,12 @@ struct ChatItemInfoView: View {
private func itemInfoShareText() -> String {
let meta = ci.meta
var shareText: [String] = [String.localizedStringWithFormat(NSLocalizedString("# %@", comment: "copied message info title, # <title>"), title), ""]
shareText += [String.localizedStringWithFormat(NSLocalizedString("Sent at: %@", comment: "copied message info"), localTimestamp(meta.itemTs))]
shareText += [String.localizedStringWithFormat(
ci.localNote
? NSLocalizedString("Created at: %@", comment: "copied message info")
: NSLocalizedString("Sent at: %@", comment: "copied message info"),
localTimestamp(meta.itemTs))
]
if !ci.chatDir.sent {
shareText += [String.localizedStringWithFormat(NSLocalizedString("Received at: %@", comment: "copied message info"), localTimestamp(meta.createdAt))]
}

View File

@@ -109,6 +109,7 @@ struct ChatItemContentView<Content: View>: View {
case let .rcvGroupFeatureRejected(feature): chatFeatureView(feature, .red)
case .sndModerated: deletedItemView()
case .rcvModerated: deletedItemView()
case .rcvBlocked: deletedItemView()
case let .invalidJSON(json): CIInvalidJSONView(json: json)
}
}

View File

@@ -151,18 +151,21 @@ struct ChatView: View {
)
)
}
} else if case .local = cInfo {
ChatInfoToolbar(chat: chat)
}
}
ToolbarItem(placement: .navigationBarTrailing) {
switch cInfo {
case let .direct(contact):
HStack {
if contact.allowsFeature(.calls) {
let callsPrefEnabled = contact.mergedPreferences.calls.enabled.forUser
if callsPrefEnabled {
callButton(contact, .audio, imageName: "phone")
.disabled(!contact.ready || !contact.active)
}
Menu {
if contact.allowsFeature(.calls) {
if callsPrefEnabled {
Button {
CallController.shared.startCall(contact, .video)
} label: {
@@ -205,6 +208,8 @@ struct ChatView: View {
Image(systemName: "ellipsis")
}
}
case .local:
searchButton()
default:
EmptyView()
}
@@ -250,8 +255,8 @@ struct ChatView: View {
}
private func searchToolbar() -> some View {
HStack {
HStack {
HStack(spacing: 12) {
HStack(spacing: 4) {
Image(systemName: "magnifyingglass")
TextField("Search", text: $searchText)
.focused($searchFocussed)
@@ -264,9 +269,9 @@ struct ChatView: View {
Image(systemName: "xmark.circle.fill").opacity(searchText == "" ? 0 : 1)
}
}
.padding(EdgeInsets(top: 8, leading: 6, bottom: 8, trailing: 6))
.padding(EdgeInsets(top: 7, leading: 7, bottom: 7, trailing: 7))
.foregroundColor(.secondary)
.background(Color(.secondarySystemBackground))
.background(Color(.tertiarySystemFill))
.cornerRadius(10.0)
Button ("Cancel") {
@@ -636,7 +641,7 @@ struct ChatView: View {
Button("Delete for me", role: .destructive) {
deleteMessage(.cidmInternal)
}
if let di = deletingItem, di.meta.editable {
if let di = deletingItem, di.meta.editable && !di.localNote {
Button(broadcastDeleteButtonText, role: .destructive) {
deleteMessage(.cidmBroadcast)
}
@@ -720,12 +725,17 @@ struct ChatView: View {
}
menu.append(rm)
}
if ci.meta.itemDeleted == nil && !ci.isLiveDummy && !live {
if ci.meta.itemDeleted == nil && !ci.isLiveDummy && !live && !ci.localNote {
menu.append(replyUIAction(ci))
}
menu.append(shareUIAction(ci))
menu.append(copyUIAction(ci))
if let fileSource = getLoadedFileSource(ci.file) {
let fileSource = getLoadedFileSource(ci.file)
let fileExists = if let fs = fileSource, FileManager.default.fileExists(atPath: getAppFilePath(fs.filePath).path) { true } else { false }
let copyAndShareAllowed = !ci.content.text.isEmpty || (ci.content.msgContent?.isImage == true && fileExists)
if copyAndShareAllowed {
menu.append(shareUIAction(ci))
menu.append(copyUIAction(ci))
}
if let fileSource = fileSource, fileExists {
if case .image = ci.content.msgContent, let image = getLoadedImage(ci.file) {
if image.imageData != nil {
menu.append(saveFileAction(fileSource))
@@ -739,13 +749,15 @@ struct ChatView: View {
if ci.meta.editable && !mc.isVoice && !live {
menu.append(editAction(ci))
}
menu.append(viewInfoUIAction(ci))
if !ci.isLiveDummy {
menu.append(viewInfoUIAction(ci))
}
if revealed {
menu.append(hideUIAction())
}
if ci.meta.itemDeleted == nil,
if ci.meta.itemDeleted == nil && !ci.localNote,
let file = ci.file,
let cancelAction = file.cancelAction {
let cancelAction = file.cancelAction {
menu.append(cancelFileUIAction(file.fileId, cancelAction))
}
if !live || !ci.meta.isLive {

View File

@@ -104,7 +104,7 @@ struct ComposeState {
var sendEnabled: Bool {
switch preview {
case .mediaPreviews: return true
case let .mediaPreviews(media): return !media.isEmpty
case .voicePreview: return voiceMessageRecordingState == .finished
case .filePreview: return true
default: return !message.isEmpty || liveMessage != nil
@@ -295,7 +295,7 @@ struct ComposeView: View {
sendMessage(ttl: ttl)
resetLinkPreview()
},
sendLiveMessage: sendLiveMessage,
sendLiveMessage: chat.chatInfo.chatType != .local ? sendLiveMessage : nil,
updateLiveMessage: updateLiveMessage,
cancelLiveMessage: {
composeState.liveMessage = nil
@@ -384,10 +384,10 @@ struct ComposeView: View {
}
}
.sheet(isPresented: $showMediaPicker) {
LibraryMediaListPicker(media: $chosenMedia, selectionLimit: 10) { itemsSelected in
showMediaPicker = false
if itemsSelected {
DispatchQueue.main.async {
LibraryMediaListPicker(addMedia: addMediaContent, selectionLimit: 10, finishedPreprocessing: finishedPreprocessingMediaContent) { itemsSelected in
await MainActor.run {
showMediaPicker = false
if itemsSelected {
composeState = composeState.copy(preview: .mediaPreviews(mediaPreviews: []))
}
}
@@ -488,6 +488,30 @@ struct ComposeView: View {
}
}
private func addMediaContent(_ content: UploadContent) async {
if let img = resizeImageToStrSize(content.uiImage, maxDataSize: 14000) {
var newMedia: [(String, UploadContent?)] = []
if case var .mediaPreviews(media) = composeState.preview {
media.append((img, content))
newMedia = media
} else {
newMedia = [(img, content)]
}
await MainActor.run {
composeState = composeState.copy(preview: .mediaPreviews(mediaPreviews: newMedia))
}
}
}
// When error occurs while converting video, remove media preview
private func finishedPreprocessingMediaContent() {
if case let .mediaPreviews(media) = composeState.preview, media.isEmpty {
DispatchQueue.main.async {
composeState = composeState.copy(preview: .noPreview)
}
}
}
private var maxFileSize: Int64 {
getMaxFileSize(.xftp)
}
@@ -665,7 +689,7 @@ struct ComposeView: View {
let file = voiceCryptoFile(recordingFileName)
sent = await send(.voice(text: msgText, duration: duration), quoted: quoted, file: file, ttl: ttl)
case let .filePreview(_, file):
if let savedFile = saveFileFromURL(file, encrypted: privacyEncryptLocalFilesGroupDefault.get()) {
if let savedFile = saveFileFromURL(file) {
sent = await send(.file(msgText), quoted: quoted, file: savedFile, live: live, ttl: ttl)
}
}
@@ -768,15 +792,17 @@ struct ComposeView: View {
}
func send(_ mc: MsgContent, quoted: Int64?, file: CryptoFile? = nil, live: Bool = false, ttl: Int?) async -> ChatItem? {
if let chatItem = await apiSendMessage(
type: chat.chatInfo.chatType,
id: chat.chatInfo.apiId,
file: file,
quotedItemId: quoted,
msg: mc,
live: live,
ttl: ttl
) {
if let chatItem = chat.chatInfo.chatType == .local
? await apiCreateChatItem(noteFolderId: chat.chatInfo.apiId, file: file, msg: mc)
: await apiSendMessage(
type: chat.chatInfo.chatType,
id: chat.chatInfo.apiId,
file: file,
quotedItemId: quoted,
msg: mc,
live: live,
ttl: ttl
) {
await MainActor.run {
chatModel.removeLiveDummy(animated: false)
chatModel.addChatItem(chat.chatInfo, chatItem)
@@ -952,6 +978,9 @@ struct ComposeView: View {
}
private func cancelLinkPreview() {
if let pendingLink = pendingLinkUrl?.absoluteString {
cancelledLinks.insert(pendingLink)
}
if let uri = composeState.linkPreview?.uri.absoluteString {
cancelledLinks.insert(uri)
}

View File

@@ -51,7 +51,8 @@ struct ContextItemView: View {
MsgContentView(
chat: chat,
text: contextItem.text,
formattedText: contextItem.formattedText
formattedText: contextItem.formattedText,
showSecrets: false
)
.multilineTextAlignment(isRightToLeft(contextItem.text) ? .trailing : .leading)
.lineLimit(lines)

View File

@@ -116,7 +116,6 @@ struct ContactPreferencesView: View {
private func featureFooter(_ feature: ChatFeature, _ enabled: FeatureEnabled) -> some View {
Text(feature.enabledDescription(enabled))
.frame(height: 36, alignment: .topLeading)
}
private func savePreferences() {

View File

@@ -16,7 +16,6 @@ struct GroupChatInfoView: View {
@Environment(\.dismiss) var dismiss: DismissAction
@ObservedObject var chat: Chat
@Binding var groupInfo: GroupInfo
@ObservedObject private var alertManager = AlertManager.shared
@State private var alert: GroupChatInfoViewAlert? = nil
@State private var groupLink: String?
@State private var groupLinkMemberRole: GroupMemberRole = .member
@@ -37,6 +36,8 @@ struct GroupChatInfoView: View {
case largeGroupReceiptsDisabled
case blockMemberAlert(mem: GroupMember)
case unblockMemberAlert(mem: GroupMember)
case blockForAllAlert(mem: GroupMember)
case unblockForAllAlert(mem: GroupMember)
case removeMemberAlert(mem: GroupMember)
case error(title: LocalizedStringKey, error: LocalizedStringKey)
@@ -49,6 +50,8 @@ struct GroupChatInfoView: View {
case .largeGroupReceiptsDisabled: return "largeGroupReceiptsDisabled"
case let .blockMemberAlert(mem): return "blockMemberAlert \(mem.groupMemberId)"
case let .unblockMemberAlert(mem): return "unblockMemberAlert \(mem.groupMemberId)"
case let .blockForAllAlert(mem): return "blockForAllAlert \(mem.groupMemberId)"
case let .unblockForAllAlert(mem): return "unblockForAllAlert \(mem.groupMemberId)"
case let .removeMemberAlert(mem): return "removeMemberAlert \(mem.groupMemberId)"
case let .error(title, _): return "error \(title)"
}
@@ -144,6 +147,8 @@ struct GroupChatInfoView: View {
case .largeGroupReceiptsDisabled: return largeGroupReceiptsDisabledAlert()
case let .blockMemberAlert(mem): return blockMemberAlert(groupInfo, mem)
case let .unblockMemberAlert(mem): return unblockMemberAlert(groupInfo, mem)
case let .blockForAllAlert(mem): return blockForAllAlert(groupInfo, mem)
case let .unblockForAllAlert(mem): return unblockForAllAlert(groupInfo, mem)
case let .removeMemberAlert(mem): return removeMemberAlert(mem)
case let .error(title, error): return Alert(title: Text(title), message: Text(error))
}
@@ -227,13 +232,10 @@ struct GroupChatInfoView: View {
.foregroundColor(.secondary)
}
Spacer()
let role = member.memberRole
if role == .owner || role == .admin {
Text(member.memberRole.text)
.foregroundColor(.secondary)
}
memberInfo(member)
}
// revert from this:
if user {
v
} else if member.canBeRemoved(groupInfo: groupInfo) {
@@ -241,6 +243,43 @@ struct GroupChatInfoView: View {
} else {
blockSwipe(member, v)
}
// revert to this: vvv
// if user {
// v
// } else if groupInfo.membership.memberRole >= .admin {
// // TODO if there are more actions, refactor with lists of swipeActions
// let canBlockForAll = member.canBlockForAll(groupInfo: groupInfo)
// let canRemove = member.canBeRemoved(groupInfo: groupInfo)
// if canBlockForAll && canRemove {
// removeSwipe(member, blockForAllSwipe(member, v))
// } else if canBlockForAll {
// blockForAllSwipe(member, v)
// } else if canRemove {
// removeSwipe(member, v)
// } else {
// v
// }
// } else {
// if !member.blockedByAdmin {
// blockSwipe(member, v)
// } else {
// v
// }
// }
// ^^^
}
@ViewBuilder private func memberInfo(_ member: GroupMember) -> some View {
if member.blocked {
Text("blocked")
.foregroundColor(.secondary)
} else {
let role = member.memberRole
if [.owner, .admin, .observer].contains(role) {
Text(member.memberRole.text)
.foregroundColor(.secondary)
}
}
}
private func blockSwipe<V: View>(_ member: GroupMember, _ v: V) -> some View {
@@ -261,6 +300,24 @@ struct GroupChatInfoView: View {
}
}
private func blockForAllSwipe<V: View>(_ member: GroupMember, _ v: V) -> some View {
v.swipeActions(edge: .leading) {
if member.blockedByAdmin {
Button {
alert = .unblockForAllAlert(mem: member)
} label: {
Label("Unblock for all", systemImage: "hand.raised.slash").foregroundColor(.accentColor)
}
} else {
Button {
alert = .blockForAllAlert(mem: member)
} label: {
Label("Block for all", systemImage: "hand.raised").foregroundColor(.secondary)
}
}
}
}
private func removeSwipe<V: View>(_ member: GroupMember, _ v: V) -> some View {
v.swipeActions(edge: .trailing) {
Button(role: .destructive) {
@@ -313,7 +370,11 @@ struct GroupChatInfoView: View {
private func addOrEditWelcomeMessage() -> some View {
NavigationLink {
GroupWelcomeView(groupId: groupInfo.groupId, groupInfo: $groupInfo)
GroupWelcomeView(
groupInfo: $groupInfo,
groupProfile: groupInfo.groupProfile,
welcomeText: groupInfo.groupProfile.description ?? ""
)
.navigationTitle("Welcome message")
.navigationBarTitleDisplayMode(.large)
} label: {

View File

@@ -18,6 +18,7 @@ struct GroupLinkView: View {
var linkCreatedCb: (() -> Void)? = nil
@State private var creatingLink = false
@State private var alert: GroupLinkAlert?
@State private var shouldCreate = true
private enum GroupLinkAlert: Identifiable {
case deleteLink
@@ -70,6 +71,7 @@ struct GroupLinkView: View {
}
.frame(height: 36)
SimpleXLinkQRCode(uri: groupLink)
.id("simplex-qrcode-view-for-\(groupLink)")
Button {
showShareSheet(items: [simplexChatLink(groupLink)])
} label: {
@@ -125,9 +127,10 @@ struct GroupLinkView: View {
}
}
.onAppear {
if groupLink == nil && !creatingLink {
if groupLink == nil && !creatingLink && shouldCreate {
createGroupLink()
}
shouldCreate = false
}
}
}

View File

@@ -27,6 +27,8 @@ struct GroupMemberInfoView: View {
enum GroupMemberInfoViewAlert: Identifiable {
case blockMemberAlert(mem: GroupMember)
case unblockMemberAlert(mem: GroupMember)
case blockForAllAlert(mem: GroupMember)
case unblockForAllAlert(mem: GroupMember)
case removeMemberAlert(mem: GroupMember)
case changeMemberRoleAlert(mem: GroupMember, role: GroupMemberRole)
case switchAddressAlert
@@ -39,6 +41,8 @@ struct GroupMemberInfoView: View {
switch self {
case let .blockMemberAlert(mem): return "blockMemberAlert \(mem.groupMemberId)"
case let .unblockMemberAlert(mem): return "unblockMemberAlert \(mem.groupMemberId)"
case let .blockForAllAlert(mem): return "blockForAllAlert \(mem.groupMemberId)"
case let .unblockForAllAlert(mem): return "unblockForAllAlert \(mem.groupMemberId)"
case let .removeMemberAlert(mem): return "removeMemberAlert \(mem.groupMemberId)"
case let .changeMemberRoleAlert(mem, role): return "changeMemberRoleAlert \(mem.groupMemberId) \(role.rawValue)"
case .switchAddressAlert: return "switchAddressAlert"
@@ -164,6 +168,7 @@ struct GroupMemberInfoView: View {
}
}
// revert from this:
Section {
if member.memberSettings.showMessages {
blockMemberButton(member)
@@ -171,9 +176,16 @@ struct GroupMemberInfoView: View {
unblockMemberButton(member)
}
if member.canBeRemoved(groupInfo: groupInfo) {
removeMemberButton(member)
removeMemberButton(member)
}
}
// revert to this: vvv
// if groupInfo.membership.memberRole >= .admin {
// adminDestructiveSection(member)
// } else {
// nonAdminBlockSection(member)
// }
// ^^^
if developerTools {
Section("For console") {
@@ -188,17 +200,19 @@ struct GroupMemberInfoView: View {
// this condition prevents re-setting picker
if !justOpened { return }
}
newRole = member.memberRole
do {
let (_, stats) = try apiGroupMemberInfo(groupInfo.apiId, member.groupMemberId)
let (mem, code) = member.memberActive ? try apiGetGroupMemberCode(groupInfo.apiId, member.groupMemberId) : (member, nil)
_ = chatModel.upsertGroupMember(groupInfo, mem)
connectionStats = stats
connectionCode = code
} catch let error {
logger.error("apiGroupMemberInfo or apiGetGroupMemberCode error: \(responseError(error))")
}
justOpened = false
DispatchQueue.main.async {
newRole = member.memberRole
do {
let (_, stats) = try apiGroupMemberInfo(groupInfo.apiId, member.groupMemberId)
let (mem, code) = member.memberActive ? try apiGetGroupMemberCode(groupInfo.apiId, member.groupMemberId) : (member, nil)
_ = chatModel.upsertGroupMember(groupInfo, mem)
connectionStats = stats
connectionCode = code
} catch let error {
logger.error("apiGroupMemberInfo or apiGetGroupMemberCode error: \(responseError(error))")
}
}
}
.onChange(of: newRole) { newRole in
if newRole != member.memberRole {
@@ -214,6 +228,8 @@ struct GroupMemberInfoView: View {
switch(alertItem) {
case let .blockMemberAlert(mem): return blockMemberAlert(groupInfo, mem)
case let .unblockMemberAlert(mem): return unblockMemberAlert(groupInfo, mem)
case let .blockForAllAlert(mem): return blockForAllAlert(groupInfo, mem)
case let .unblockForAllAlert(mem): return unblockForAllAlert(groupInfo, mem)
case let .removeMemberAlert(mem): return removeMemberAlert(mem)
case let .changeMemberRoleAlert(mem, _): return changeMemberRoleAlert(mem)
case .switchAddressAlert: return switchAddressAlert(switchMemberAddress)
@@ -383,6 +399,55 @@ struct GroupMemberInfoView: View {
}
}
@ViewBuilder private func adminDestructiveSection(_ mem: GroupMember) -> some View {
let canBlockForAll = mem.canBlockForAll(groupInfo: groupInfo)
let canRemove = mem.canBeRemoved(groupInfo: groupInfo)
if canBlockForAll || canRemove {
Section {
if canBlockForAll {
if mem.blockedByAdmin {
unblockForAllButton(mem)
} else {
blockForAllButton(mem)
}
}
if canRemove {
removeMemberButton(mem)
}
}
}
}
private func nonAdminBlockSection(_ mem: GroupMember) -> some View {
Section {
if mem.blockedByAdmin {
Label("Blocked by admin", systemImage: "hand.raised")
.foregroundColor(.secondary)
} else if mem.memberSettings.showMessages {
blockMemberButton(mem)
} else {
unblockMemberButton(mem)
}
}
}
private func blockForAllButton(_ mem: GroupMember) -> some View {
Button(role: .destructive) {
alert = .blockForAllAlert(mem: mem)
} label: {
Label("Block for all", systemImage: "hand.raised")
.foregroundColor(.red)
}
}
private func unblockForAllButton(_ mem: GroupMember) -> some View {
Button {
alert = .unblockForAllAlert(mem: mem)
} label: {
Label("Unblock for all", systemImage: "hand.raised.slash")
}
}
private func blockMemberButton(_ mem: GroupMember) -> some View {
Button(role: .destructive) {
alert = .blockMemberAlert(mem: mem)
@@ -558,6 +623,41 @@ func updateMemberSettings(_ gInfo: GroupInfo, _ member: GroupMember, _ memberSet
}
}
func blockForAllAlert(_ gInfo: GroupInfo, _ mem: GroupMember) -> Alert {
Alert(
title: Text("Block member for all?"),
message: Text("All new messages from \(mem.chatViewName) will be hidden!"),
primaryButton: .destructive(Text("Block for all")) {
blockMemberForAll(gInfo, mem, true)
},
secondaryButton: .cancel()
)
}
func unblockForAllAlert(_ gInfo: GroupInfo, _ mem: GroupMember) -> Alert {
Alert(
title: Text("Unblock member for all?"),
message: Text("Messages from \(mem.chatViewName) will be shown!"),
primaryButton: .default(Text("Unblock for all")) {
blockMemberForAll(gInfo, mem, false)
},
secondaryButton: .cancel()
)
}
func blockMemberForAll(_ gInfo: GroupInfo, _ member: GroupMember, _ blocked: Bool) {
Task {
do {
let updatedMember = try await apiBlockMemberForAll(gInfo.groupId, member.groupMemberId, blocked)
await MainActor.run {
_ = ChatModel.shared.upsertGroupMember(gInfo, updatedMember)
}
} catch let error {
logger.error("apiBlockMemberForAll error: \(responseError(error))")
}
}
}
struct GroupMemberInfoView_Previews: PreviewProvider {
static var previews: some View {
GroupMemberInfoView(

View File

@@ -28,6 +28,7 @@ struct GroupPreferencesView: View {
featureSection(.reactions, $preferences.reactions.enable)
featureSection(.voice, $preferences.voice.enable)
featureSection(.files, $preferences.files.enable)
featureSection(.history, $preferences.history.enable)
if groupInfo.canEdit {
Section {
@@ -96,7 +97,6 @@ struct GroupPreferencesView: View {
}
} footer: {
Text(feature.enableDescription(enableFeature.wrappedValue, groupInfo.canEdit))
.frame(height: 36, alignment: .topLeading)
}
}

View File

@@ -103,8 +103,10 @@ struct GroupProfileView: View {
}
}
.sheet(isPresented: $showImagePicker) {
LibraryImagePicker(image: $chosenImage) {
didSelectItem in showImagePicker = false
LibraryImagePicker(image: $chosenImage) { _ in
await MainActor.run {
showImagePicker = false
}
}
}
.onChange(of: chosenImage) { image in

View File

@@ -11,29 +11,32 @@ import SimpleXChat
struct GroupWelcomeView: View {
@Environment(\.dismiss) var dismiss: DismissAction
@EnvironmentObject private var m: ChatModel
var groupId: Int64
@Binding var groupInfo: GroupInfo
@State private var welcomeText: String = ""
@State var groupProfile: GroupProfile
@State var welcomeText: String
@State private var editMode = true
@FocusState private var keyboardVisible: Bool
@State private var showSaveDialog = false
let maxByteCount = 1200
var body: some View {
VStack {
if groupInfo.canEdit {
editorView()
.modifier(BackButton {
if welcomeText == groupInfo.groupProfile.description || (welcomeText == "" && groupInfo.groupProfile.description == nil) {
if welcomeTextUnchanged() {
dismiss()
} else {
showSaveDialog = true
}
})
.confirmationDialog("Save welcome message?", isPresented: $showSaveDialog) {
Button("Save and update group profile") {
save()
dismiss()
.confirmationDialog(
welcomeTextFitsLimit() ? "Save welcome message?" : "Welcome message is too long",
isPresented: $showSaveDialog
) {
if welcomeTextFitsLimit() {
Button("Save and update group profile") { save() }
}
Button("Exit without saving") { dismiss() }
}
@@ -47,15 +50,15 @@ struct GroupWelcomeView: View {
}
}
.onAppear {
welcomeText = groupInfo.groupProfile.description ?? ""
keyboardVisible = true
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
keyboardVisible = true
}
}
}
private func textPreview() -> some View {
messageText(welcomeText, parseSimpleXMarkdown(welcomeText), nil)
.allowsHitTesting(false)
.frame(minHeight: 140, alignment: .topLeading)
messageText(welcomeText, parseSimpleXMarkdown(welcomeText), nil, showSecrets: false)
.frame(minHeight: 130, alignment: .topLeading)
.frame(maxWidth: .infinity, alignment: .leading)
}
@@ -75,7 +78,7 @@ struct GroupWelcomeView: View {
}
.padding(.horizontal, -5)
.padding(.top, -8)
.frame(height: 140, alignment: .topLeading)
.frame(height: 130, alignment: .topLeading)
.frame(maxWidth: .infinity, alignment: .leading)
}
} else {
@@ -94,6 +97,9 @@ struct GroupWelcomeView: View {
}
.disabled(welcomeText.isEmpty)
copyButton()
} footer: {
Text(!welcomeTextFitsLimit() ? "Message too large" : "")
.foregroundColor(.red)
}
Section {
@@ -114,7 +120,15 @@ struct GroupWelcomeView: View {
Button("Save and update group profile") {
save()
}
.disabled(welcomeText == groupInfo.groupProfile.description || (welcomeText == "" && groupInfo.groupProfile.description == nil))
.disabled(welcomeTextUnchanged() || !welcomeTextFitsLimit())
}
private func welcomeTextUnchanged() -> Bool {
welcomeText == groupInfo.groupProfile.description || (welcomeText == "" && groupInfo.groupProfile.description == nil)
}
private func welcomeTextFitsLimit() -> Bool {
chatJsonLength(welcomeText) <= maxByteCount
}
private func save() {
@@ -124,11 +138,13 @@ struct GroupWelcomeView: View {
if welcome?.count == 0 {
welcome = nil
}
var groupProfileUpdated = groupInfo.groupProfile
groupProfileUpdated.description = welcome
groupInfo = try await apiUpdateGroup(groupId, groupProfileUpdated)
m.updateGroup(groupInfo)
welcomeText = welcome ?? ""
groupProfile.description = welcome
let gInfo = try await apiUpdateGroup(groupInfo.groupId, groupProfile)
await MainActor.run {
groupInfo = gInfo
ChatModel.shared.updateGroup(gInfo)
dismiss()
}
} catch let error {
logger.error("apiUpdateGroup error: \(responseError(error))")
}
@@ -138,6 +154,6 @@ struct GroupWelcomeView: View {
struct GroupWelcomeView_Previews: PreviewProvider {
static var previews: some View {
GroupWelcomeView(groupId: 1, groupInfo: Binding.constant(GroupInfo.sampleData))
GroupProfileView(groupInfo: Binding.constant(GroupInfo.sampleData), groupProfile: GroupProfile.sampleData)
}
}

View File

@@ -17,7 +17,7 @@ struct ScanCodeView: View {
var body: some View {
VStack(alignment: .leading) {
CodeScannerView(codeTypes: [.qr], completion: processQRCode)
CodeScannerView(codeTypes: [.qr], scanMode: .oncePerCode, completion: processQRCode)
.aspectRatio(1, contentMode: .fit)
.cornerRadius(12)
Text("Scan security code from your contact's app.")

View File

@@ -11,7 +11,7 @@ import SwiftUI
struct ChatHelp: View {
@EnvironmentObject var chatModel: ChatModel
@Binding var showSettings: Bool
@State private var showAddChat = false
@State private var newChatMenuOption: NewChatMenuOption? = nil
var body: some View {
ScrollView { chatHelp() }
@@ -39,13 +39,12 @@ struct ChatHelp: View {
HStack(spacing: 8) {
Text("Tap button ")
NewChatButton(showAddChat: $showAddChat)
NewChatMenuButton(newChatMenuOption: $newChatMenuOption)
Text("above, then choose:")
}
Text("**Create link / QR code** for your contact to use.")
Text("**Paste received link** or open it in the browser and tap **Open in mobile app**.")
Text("**Scan QR code**: to connect to your contact in person or via video call.")
Text("**Add contact**: to create a new invitation link, or connect via a link you received.")
Text("**Create group**: to create a new group.")
}
.padding(.top, 24)

View File

@@ -44,6 +44,8 @@ struct ChatListNavLink: View {
contactNavLink(contact)
case let .group(groupInfo):
groupNavLink(groupInfo)
case let .local(noteFolder):
noteFolderNavLink(noteFolder)
case let .contactRequest(cReq):
contactRequestNavLink(cReq)
case let .contactConnection(cConn):
@@ -195,6 +197,24 @@ struct ChatListNavLink: View {
}
}
@ViewBuilder private func noteFolderNavLink(_ noteFolder: NoteFolder) -> some View {
NavLinkPlain(
tag: chat.chatInfo.id,
selection: $chatModel.chatId,
label: { ChatPreviewView(chat: chat, progressByTimeout: Binding.constant(false)) },
disabled: !noteFolder.ready
)
.frame(height: rowHeights[dynamicTypeSize])
.swipeActions(edge: .leading, allowsFullSwipe: true) {
markReadButton()
}
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
if !chat.chatItems.isEmpty {
clearNoteFolderButton()
}
}
}
private func joinGroupButton() -> some View {
Button {
inProgress = true
@@ -253,6 +273,15 @@ struct ChatListNavLink: View {
.tint(Color.orange)
}
private func clearNoteFolderButton() -> some View {
Button {
AlertManager.shared.showAlert(clearNoteFolderAlert())
} label: {
Label("Clear", systemImage: "gobackward")
}
.tint(Color.orange)
}
private func leaveGroupChatButton(_ groupInfo: GroupInfo) -> some View {
Button {
AlertManager.shared.showAlert(leaveGroupAlert(groupInfo))
@@ -357,6 +386,17 @@ struct ChatListNavLink: View {
)
}
private func clearNoteFolderAlert() -> Alert {
Alert(
title: Text("Clear private notes?"),
message: Text("All messages will be deleted - this cannot be undone!"),
primaryButton: .destructive(Text("Clear")) {
Task { await clearChat(chat) }
},
secondaryButton: .cancel()
)
}
private func leaveGroupAlert(_ groupInfo: GroupInfo) -> Alert {
Alert(
title: Text("Leave group?"),

View File

@@ -12,8 +12,12 @@ import SimpleXChat
struct ChatListView: View {
@EnvironmentObject var chatModel: ChatModel
@Binding var showSettings: Bool
@State private var searchMode = false
@FocusState private var searchFocussed
@State private var searchText = ""
@State private var showAddChat = false
@State private var searchShowingSimplexLink = false
@State private var searchChatFilteredBySimplexLink: String? = nil
@State private var newChatMenuOption: NewChatMenuOption? = nil
@State private var userPickerVisible = false
@State private var showConnectDesktop = false
@AppStorage(DEFAULT_SHOW_UNREAD_AND_FAVORITES) private var showUnreadAndFavorites = false
@@ -62,11 +66,7 @@ struct ChatListView: View {
private var chatListView: some View {
VStack {
if chatModel.chats.count > 0 {
chatList.searchable(text: $searchText)
} else {
chatList
}
chatList
}
.onDisappear() { withAnimation { userPickerVisible = false } }
.refreshable {
@@ -85,9 +85,9 @@ struct ChatListView: View {
secondaryButton: .cancel()
))
}
.offset(x: -8)
.listStyle(.plain)
.navigationBarTitleDisplayMode(.inline)
.navigationBarHidden(searchMode)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
let user = chatModel.currentUser ?? User.sampleData
@@ -124,7 +124,7 @@ struct ChatListView: View {
}
ToolbarItem(placement: .navigationBarTrailing) {
switch chatModel.chatRunning {
case .some(true): NewChatButton(showAddChat: $showAddChat)
case .some(true): NewChatMenuButton(newChatMenuOption: $newChatMenuOption)
case .some(false): chatStoppedIcon()
case .none: EmptyView()
}
@@ -144,11 +144,25 @@ struct ChatListView: View {
@ViewBuilder private var chatList: some View {
let cs = filteredChats()
ZStack {
List {
ForEach(cs, id: \.viewId) { chat in
ChatListNavLink(chat: chat)
.padding(.trailing, -16)
.disabled(chatModel.chatRunning != true)
VStack {
List {
if !chatModel.chats.isEmpty {
ChatListSearchBar(
searchMode: $searchMode,
searchFocussed: $searchFocussed,
searchText: $searchText,
searchShowingSimplexLink: $searchShowingSimplexLink,
searchChatFilteredBySimplexLink: $searchChatFilteredBySimplexLink
)
.listRowSeparator(.hidden)
.frame(maxWidth: .infinity)
}
ForEach(cs, id: \.viewId) { chat in
ChatListNavLink(chat: chat)
.padding(.trailing, -16)
.disabled(chatModel.chatRunning != true || chatModel.deletedChats.contains(chat.chatInfo.id))
}
.offset(x: -8)
}
}
.onChange(of: chatModel.chatId) { _ in
@@ -182,7 +196,7 @@ struct ChatListView: View {
.padding(.trailing, 12)
connectButton("Tap to start a new chat") {
showAddChat = true
newChatMenuOption = .newContact
}
Spacer()
@@ -214,22 +228,27 @@ struct ChatListView: View {
}
private func filteredChats() -> [Chat] {
let s = searchText.trimmingCharacters(in: .whitespaces).localizedLowercase
return s == "" && !showUnreadAndFavorites
if let linkChatId = searchChatFilteredBySimplexLink {
return chatModel.chats.filter { $0.id == linkChatId }
} else {
let s = searchString()
return s == "" && !showUnreadAndFavorites
? chatModel.chats
: chatModel.chats.filter { chat in
let cInfo = chat.chatInfo
switch cInfo {
case let .direct(contact):
return s == ""
? filtered(chat)
: (viewNameContains(cInfo, s) ||
contact.profile.displayName.localizedLowercase.contains(s) ||
contact.fullName.localizedLowercase.contains(s))
? filtered(chat)
: (viewNameContains(cInfo, s) ||
contact.profile.displayName.localizedLowercase.contains(s) ||
contact.fullName.localizedLowercase.contains(s))
case let .group(gInfo):
return s == ""
? (filtered(chat) || gInfo.membership.memberStatus == .memInvited)
: viewNameContains(cInfo, s)
? (filtered(chat) || gInfo.membership.memberStatus == .memInvited)
: viewNameContains(cInfo, s)
case .local:
return s == "" || viewNameContains(cInfo, s)
case .contactRequest:
return s == "" || viewNameContains(cInfo, s)
case let .contactConnection(conn):
@@ -238,6 +257,11 @@ struct ChatListView: View {
return false
}
}
}
func searchString() -> String {
searchShowingSimplexLink ? "" : searchText.trimmingCharacters(in: .whitespaces).localizedLowercase
}
func filtered(_ chat: Chat) -> Bool {
(chat.chatInfo.chatSettings?.favorite ?? false) || chat.chatStats.unreadCount > 0 || chat.chatStats.unreadChat
@@ -249,6 +273,121 @@ struct ChatListView: View {
}
}
struct ChatListSearchBar: View {
@EnvironmentObject var m: ChatModel
@Binding var searchMode: Bool
@FocusState.Binding var searchFocussed: Bool
@Binding var searchText: String
@Binding var searchShowingSimplexLink: Bool
@Binding var searchChatFilteredBySimplexLink: String?
@State private var ignoreSearchTextChange = false
@State private var showScanCodeSheet = false
@State private var alert: PlanAndConnectAlert?
@State private var sheet: PlanAndConnectActionSheet?
var body: some View {
VStack(spacing: 12) {
HStack(spacing: 12) {
HStack(spacing: 4) {
Image(systemName: "magnifyingglass")
TextField("Search or paste SimpleX link", text: $searchText)
.foregroundColor(searchShowingSimplexLink ? .secondary : .primary)
.disabled(searchShowingSimplexLink)
.focused($searchFocussed)
.frame(maxWidth: .infinity)
if !searchText.isEmpty {
Image(systemName: "xmark.circle.fill")
.onTapGesture {
searchText = ""
}
} else if !searchFocussed {
HStack(spacing: 24) {
if m.pasteboardHasStrings {
Image(systemName: "doc")
.onTapGesture {
if let str = UIPasteboard.general.string {
searchText = str
}
}
}
Image(systemName: "qrcode")
.resizable()
.scaledToFit()
.frame(width: 20, height: 20)
.onTapGesture {
showScanCodeSheet = true
}
}
.padding(.trailing, 2)
}
}
.padding(EdgeInsets(top: 7, leading: 7, bottom: 7, trailing: 7))
.foregroundColor(.secondary)
.background(Color(.tertiarySystemFill))
.cornerRadius(10.0)
if searchFocussed {
Text("Cancel")
.foregroundColor(.accentColor)
.onTapGesture {
searchText = ""
searchFocussed = false
}
}
}
Divider()
}
.sheet(isPresented: $showScanCodeSheet) {
NewChatView(selection: .connect, showQRCodeScanner: true)
.environment(\EnvironmentValues.refresh as! WritableKeyPath<EnvironmentValues, RefreshAction?>, nil) // fixes .refreshable in ChatListView affecting nested view
}
.onChange(of: searchFocussed) { sf in
withAnimation { searchMode = sf }
}
.onChange(of: searchText) { t in
if ignoreSearchTextChange {
ignoreSearchTextChange = false
} else {
if let link = strHasSingleSimplexLink(t.trimmingCharacters(in: .whitespaces)) { // if SimpleX link is pasted, show connection dialogue
searchFocussed = false
if case let .simplexLink(linkType, _, smpHosts) = link.format {
ignoreSearchTextChange = true
searchText = simplexLinkText(linkType, smpHosts)
}
searchShowingSimplexLink = true
searchChatFilteredBySimplexLink = nil
connect(link.text)
} else {
if t != "" { // if some other text is pasted, enter search mode
searchFocussed = true
}
searchShowingSimplexLink = false
searchChatFilteredBySimplexLink = nil
}
}
}
.alert(item: $alert) { a in
planAndConnectAlert(a, dismiss: true, cleanup: { searchText = "" })
}
.actionSheet(item: $sheet) { s in
planAndConnectActionSheet(s, dismiss: true, cleanup: { searchText = "" })
}
}
private func connect(_ link: String) {
planAndConnect(
link,
showAlert: { alert = $0 },
showActionSheet: { sheet = $0 },
dismiss: false,
incognito: nil,
filterKnownContact: { searchChatFilteredBySimplexLink = $0.id },
filterKnownGroup: { searchChatFilteredBySimplexLink = $0.id }
)
}
}
func chatStoppedIcon() -> some View {
Button {
AlertManager.shared.showAlertMsg(

View File

@@ -13,6 +13,7 @@ struct ChatPreviewView: View {
@EnvironmentObject var chatModel: ChatModel
@ObservedObject var chat: Chat
@Binding var progressByTimeout: Bool
@State var deleting: Bool = false
@Environment(\.colorScheme) var colorScheme
var darkGreen = Color(red: 0, green: 0.5, blue: 0)
@@ -33,7 +34,7 @@ struct ChatPreviewView: View {
HStack(alignment: .top) {
chatPreviewTitle()
Spacer()
(cItem?.timestampText ?? formatTimestampText(chat.chatInfo.updatedAt))
(cItem?.timestampText ?? formatTimestampText(chat.chatInfo.chatTs))
.font(.subheadline)
.frame(minWidth: 60, alignment: .trailing)
.foregroundColor(.secondary)
@@ -55,6 +56,9 @@ struct ChatPreviewView: View {
.frame(maxHeight: .infinity)
}
.padding(.bottom, -8)
.onChange(of: chatModel.deletedChats.contains(chat.chatInfo.id)) { contains in
deleting = contains
}
}
@ViewBuilder private func chatPreviewImageOverlayIcon() -> some View {
@@ -87,13 +91,13 @@ struct ChatPreviewView: View {
let t = Text(chat.chatInfo.chatViewName).font(.title3).fontWeight(.bold)
switch chat.chatInfo {
case let .direct(contact):
previewTitle(contact.verified == true ? verifiedIcon + t : t)
previewTitle(contact.verified == true ? verifiedIcon + t : t).foregroundColor(deleting ? Color.secondary : nil)
case let .group(groupInfo):
let v = previewTitle(t)
switch (groupInfo.membership.memberStatus) {
case .memInvited: v.foregroundColor(chat.chatInfo.incognito ? .indigo : .accentColor)
case .memInvited: v.foregroundColor(deleting ? .secondary : chat.chatInfo.incognito ? .indigo : .accentColor)
case .memAccepted: v.foregroundColor(.secondary)
default: v
default: if deleting { v.foregroundColor(.secondary) } else { v }
}
default: previewTitle(t)
}
@@ -130,9 +134,9 @@ struct ChatPreviewView: View {
.foregroundColor(.white)
.padding(.horizontal, 4)
.frame(minWidth: 18, minHeight: 18)
.background(chat.chatInfo.ntfsEnabled ? Color.accentColor : Color.secondary)
.background(chat.chatInfo.ntfsEnabled || chat.chatInfo.chatType == .local ? Color.accentColor : Color.secondary)
.cornerRadius(10)
} else if !chat.chatInfo.ntfsEnabled {
} else if !chat.chatInfo.ntfsEnabled && chat.chatInfo.chatType != .local {
Image(systemName: "speaker.slash.fill")
.foregroundColor(.secondary)
} else if chat.chatInfo.chatSettings?.favorite ?? false {
@@ -150,7 +154,7 @@ struct ChatPreviewView: View {
let msg = draft.message
return image("rectangle.and.pencil.and.ellipsis", color: .accentColor)
+ attachment()
+ messageText(msg, parseSimpleXMarkdown(msg), nil, preview: true)
+ messageText(msg, parseSimpleXMarkdown(msg), nil, preview: true, showSecrets: false)
func image(_ s: String, color: Color = Color(uiColor: .tertiaryLabel)) -> Text {
Text(Image(systemName: s)).foregroundColor(color) + Text(" ")
@@ -167,9 +171,20 @@ struct ChatPreviewView: View {
}
func chatItemPreview(_ cItem: ChatItem) -> Text {
let itemText = cItem.meta.itemDeleted == nil ? cItem.text : NSLocalizedString("marked deleted", comment: "marked deleted chat item preview text")
let itemText = cItem.meta.itemDeleted == nil ? cItem.text : markedDeletedText()
let itemFormattedText = cItem.meta.itemDeleted == nil ? cItem.formattedText : nil
return messageText(itemText, itemFormattedText, cItem.memberDisplayName, icon: attachment(), preview: true)
return messageText(itemText, itemFormattedText, cItem.memberDisplayName, icon: attachment(), preview: true, showSecrets: false)
// same texts are in markedDeletedText in MarkedDeletedItemView, but it returns LocalizedStringKey;
// can be refactored into a single function if functions calling these are changed to return same type
func markedDeletedText() -> String {
switch cItem.meta.itemDeleted {
case let .moderated(_, byGroupMember): String.localizedStringWithFormat(NSLocalizedString("moderated by %@", comment: "marked deleted chat item preview text"), byGroupMember.displayName)
case .blocked: NSLocalizedString("blocked", comment: "marked deleted chat item preview text")
case .blockedByAdmin: NSLocalizedString("blocked by admin", comment: "marked deleted chat item preview text")
case .deleted, nil: NSLocalizedString("marked deleted", comment: "marked deleted chat item preview text")
}
}
func attachment() -> String? {
switch cItem.content.msgContent {

View File

@@ -164,6 +164,28 @@ struct ContactConnectionInfo: View {
}
}
private func shareLinkButton(_ connReqInvitation: String) -> some View {
Button {
showShareSheet(items: [simplexChatLink(connReqInvitation)])
} label: {
settingsRow("square.and.arrow.up") {
Text("Share 1-time link")
}
}
}
private func oneTimeLinkLearnMoreButton() -> some View {
NavigationLink {
AddContactLearnMore(showTitle: false)
.navigationTitle("One-time invitation link")
.navigationBarTitleDisplayMode(.large)
} label: {
settingsRow("info.circle") {
Text("Learn more")
}
}
}
struct ContactConnectionInfo_Previews: PreviewProvider {
static var previews: some View {
ContactConnectionInfo(contactConnection: PendingContactConnection.getSampleData())

View File

@@ -149,7 +149,7 @@ struct DatabaseErrorView: View {
private func runChatSync(confirmMigrations: MigrationConfirmation? = nil) {
do {
resetChatCtrl()
try initializeChat(start: m.v3DBMigration.startChat, dbKey: useKeychain ? nil : dbKey, confirmMigrations: confirmMigrations)
try initializeChat(start: m.v3DBMigration.startChat, confirmStart: m.v3DBMigration.startChat && AppChatState.shared.value == .stopped, dbKey: useKeychain ? nil : dbKey, confirmMigrations: confirmMigrations)
if let s = m.chatDbStatus {
status = s
let am = AlertManager.shared

View File

@@ -415,7 +415,7 @@ struct DatabaseView: View {
do {
try initializeChat(start: true)
m.chatDbChanged = false
appStateGroupDefault.set(.active)
AppChatState.shared.set(.active)
} catch let error {
fatalError("Error starting chat \(responseError(error))")
}
@@ -427,7 +427,7 @@ struct DatabaseView: View {
m.chatRunning = true
ChatReceiver.shared.start()
chatLastStartGroupDefault.set(Date.now)
appStateGroupDefault.set(.active)
AppChatState.shared.set(.active)
} catch let error {
runChat = false
alert = .error(title: "Error starting chat", error: responseError(error))
@@ -477,13 +477,14 @@ func stopChatAsync() async throws {
try await apiStopChat()
ChatReceiver.shared.stop()
await MainActor.run { ChatModel.shared.chatRunning = false }
appStateGroupDefault.set(.stopped)
AppChatState.shared.set(.stopped)
}
func deleteChatAsync() async throws {
try await apiDeleteStorage()
_ = kcDatabasePassword.remove()
storeDBPassphraseGroupDefault.set(true)
deleteAppDatabaseAndFiles()
}
struct DatabaseView_Previews: PreviewProvider {

View File

@@ -10,6 +10,7 @@ import SwiftUI
import SimpleXChat
struct ChatInfoImage: View {
@Environment(\.colorScheme) var colorScheme
@ObservedObject var chat: Chat
var color = Color(uiColor: .tertiarySystemGroupedBackground)
@@ -18,13 +19,16 @@ struct ChatInfoImage: View {
switch chat.chatInfo {
case .direct: iconName = "person.crop.circle.fill"
case .group: iconName = "person.2.circle.fill"
case .local: iconName = "folder.circle.fill"
case .contactRequest: iconName = "person.crop.circle.fill"
default: iconName = "circle.fill"
}
let notesColor = colorScheme == .light ? notesChatColorLight : notesChatColorDark
let iconColor = if case .local = chat.chatInfo { notesColor } else { color }
return ProfileImage(
imageStr: chat.chatInfo.image,
iconName: iconName,
color: color
color: iconColor
)
}
}

View File

@@ -13,112 +13,130 @@ import SimpleXChat
struct LibraryImagePicker: View {
@Binding var image: UIImage?
var didFinishPicking: (_ didSelectItems: Bool) -> Void
@State var images: [UploadContent] = []
var didFinishPicking: (_ didSelectImage: Bool) async -> Void
@State var mediaAdded = false
var body: some View {
LibraryMediaListPicker(media: $images, selectionLimit: 1, didFinishPicking: didFinishPicking)
.onChange(of: images) { _ in
if let img = images.first {
image = img.uiImage
}
}
LibraryMediaListPicker(addMedia: addMedia, selectionLimit: 1, didFinishPicking: didFinishPicking)
}
private func addMedia(_ content: UploadContent) async {
if mediaAdded { return }
await MainActor.run {
mediaAdded = true
image = content.uiImage
}
}
}
struct LibraryMediaListPicker: UIViewControllerRepresentable {
typealias UIViewControllerType = PHPickerViewController
@Binding var media: [UploadContent]
var addMedia: (_ content: UploadContent) async -> Void
var selectionLimit: Int
var didFinishPicking: (_ didSelectItems: Bool) -> Void
var finishedPreprocessing: () -> Void = {}
var didFinishPicking: (_ didSelectItems: Bool) async -> Void
class Coordinator: PHPickerViewControllerDelegate {
let parent: LibraryMediaListPicker
let dispatchQueue = DispatchQueue(label: "chat.simplex.app.LibraryMediaListPicker")
var media: [UploadContent] = []
var mediaCount: Int = 0
init(_ parent: LibraryMediaListPicker) {
self.parent = parent
}
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
parent.didFinishPicking(!results.isEmpty)
guard !results.isEmpty else {
return
Task {
await parent.didFinishPicking(!results.isEmpty)
if results.isEmpty { return }
for r in results {
await loadItem(r.itemProvider)
}
parent.finishedPreprocessing()
}
}
parent.media = []
media = []
mediaCount = results.count
for result in results {
logger.log("LibraryMediaListPicker result")
let p = result.itemProvider
if p.hasItemConformingToTypeIdentifier(UTType.movie.identifier) {
p.loadFileRepresentation(forTypeIdentifier: UTType.movie.identifier) { url, error in
if let url = url {
let tempUrl = URL(fileURLWithPath: getTempFilesDirectory().path + "/" + generateNewFileName("video", url.pathExtension))
if ((try? FileManager.default.copyItem(at: url, to: tempUrl)) != nil) {
ChatModel.shared.filesToDelete.insert(tempUrl)
self.loadVideo(url: tempUrl, error: error)
private func loadItem(_ p: NSItemProvider) async {
logger.debug("LibraryMediaListPicker result")
if p.hasItemConformingToTypeIdentifier(UTType.movie.identifier) {
if let video = await loadVideo(p) {
await self.parent.addMedia(video)
logger.debug("LibraryMediaListPicker: added video")
}
} else if p.hasItemConformingToTypeIdentifier(UTType.data.identifier) {
if let img = await loadImageData(p) {
await self.parent.addMedia(img)
logger.debug("LibraryMediaListPicker: added image")
}
} else if p.canLoadObject(ofClass: UIImage.self) {
if let img = await loadImage(p) {
await self.parent.addMedia(.simpleImage(image: img))
logger.debug("LibraryMediaListPicker: added image")
}
}
}
private func loadImageData(_ p: NSItemProvider) async -> UploadContent? {
await withCheckedContinuation { cont in
loadFileURL(p, type: UTType.data) { url in
if let url = url {
let img = UploadContent.loadFromURL(url: url)
cont.resume(returning: img)
} else {
cont.resume(returning: nil)
}
}
}
}
private func loadImage(_ p: NSItemProvider) async -> UIImage? {
await withCheckedContinuation { cont in
p.loadObject(ofClass: UIImage.self) { obj, err in
if let err = err {
logger.error("LibraryMediaListPicker result image error: \(err.localizedDescription)")
cont.resume(returning: nil)
} else {
cont.resume(returning: obj as? UIImage)
}
}
}
}
private func loadVideo(_ p: NSItemProvider) async -> UploadContent? {
await withCheckedContinuation { cont in
loadFileURL(p, type: UTType.movie) { url in
if let url = url {
let tempUrl = URL(fileURLWithPath: generateNewFileName(getTempFilesDirectory().path + "/" + "rawvideo", url.pathExtension, fullPath: true))
let convertedVideoUrl = URL(fileURLWithPath: generateNewFileName(getTempFilesDirectory().path + "/" + "video", "mp4", fullPath: true))
do {
// logger.debug("LibraryMediaListPicker copyItem \(url) to \(tempUrl)")
try FileManager.default.copyItem(at: url, to: tempUrl)
} catch let err {
logger.error("LibraryMediaListPicker copyItem error: \(err.localizedDescription)")
return cont.resume(returning: nil)
}
Task {
let success = await makeVideoQualityLower(tempUrl, outputUrl: convertedVideoUrl)
try? FileManager.default.removeItem(at: tempUrl)
if success {
_ = ChatModel.shared.filesToDelete.insert(convertedVideoUrl)
let video = UploadContent.loadVideoFromURL(url: convertedVideoUrl)
return cont.resume(returning: video)
}
try? FileManager.default.removeItem(at: convertedVideoUrl)
cont.resume(returning: nil)
}
}
} else if p.hasItemConformingToTypeIdentifier(UTType.data.identifier) {
p.loadFileRepresentation(forTypeIdentifier: UTType.data.identifier) { url, error in
self.loadImage(object: url, error: error)
}
} else if p.canLoadObject(ofClass: UIImage.self) {
p.loadObject(ofClass: UIImage.self) { image, error in
DispatchQueue.main.async {
self.loadImage(object: image, error: error)
}
}
}
}
}
private func loadFileURL(_ p: NSItemProvider, type: UTType, completion: @escaping (URL?) -> Void) {
p.loadFileRepresentation(forTypeIdentifier: type.identifier) { url, err in
if let err = err {
logger.error("LibraryMediaListPicker loadFileURL error: \(err.localizedDescription)")
completion(nil)
} else {
dispatchQueue.sync { self.mediaCount -= 1}
}
}
DispatchQueue.main.asyncAfter(deadline: .now() + 10) {
self.dispatchQueue.sync {
if self.parent.media.count == 0 {
logger.log("LibraryMediaListPicker: added \(self.media.count) images out of \(results.count)")
self.parent.media = self.media
}
}
}
}
func loadImage(object: Any?, error: Error? = nil) {
if let error = error {
logger.error("LibraryMediaListPicker: couldn't load image with error: \(error.localizedDescription)")
} else if let image = object as? UIImage {
media.append(.simpleImage(image: image))
logger.log("LibraryMediaListPicker: added image")
} else if let url = object as? URL, let image = UploadContent.loadFromURL(url: url) {
media.append(image)
}
dispatchQueue.sync {
self.mediaCount -= 1
if self.mediaCount == 0 && self.parent.media.count == 0 {
logger.log("LibraryMediaListPicker: added all media")
self.parent.media = self.media
self.media = []
}
}
}
func loadVideo(url: URL?, error: Error? = nil) {
if let error = error {
logger.error("LibraryMediaListPicker: couldn't load video with error: \(error.localizedDescription)")
} else if let url = url as URL?, let video = UploadContent.loadVideoFromURL(url: url) {
media.append(video)
}
dispatchQueue.sync {
self.mediaCount -= 1
if self.mediaCount == 0 && self.parent.media.count == 0 {
logger.log("LibraryMediaListPicker: added all media")
self.parent.media = self.media
self.media = []
completion(url)
}
}
}

View File

@@ -0,0 +1,26 @@
//
// VideoUtils.swift
// SimpleX (iOS)
//
// Created by Avently on 25.12.2023.
// Copyright © 2023 SimpleX Chat. All rights reserved.
//
import AVFoundation
import Foundation
import SimpleXChat
func makeVideoQualityLower(_ input: URL, outputUrl: URL) async -> Bool {
let asset: AVURLAsset = AVURLAsset(url: input, options: nil)
if let s = AVAssetExportSession(asset: asset, presetName: AVAssetExportPreset640x480) {
s.outputURL = outputUrl
s.outputFileType = .mp4
s.metadataItemFilter = AVMetadataItemFilter.forSharing()
await s.export()
if let err = s.error {
logger.error("Failed to export video with error: \(err)")
}
return s.status == .completed
}
return false
}

View File

@@ -13,19 +13,28 @@ struct LocalAuthView: View {
@EnvironmentObject var m: ChatModel
var authRequest: LocalAuthRequest
@State private var password = ""
@State private var allowToReact = true
var body: some View {
PasscodeView(passcode: $password, title: authRequest.title ?? "Enter Passcode", reason: authRequest.reason, submitLabel: "Submit") {
PasscodeView(passcode: $password, title: authRequest.title ?? "Enter Passcode", reason: authRequest.reason, submitLabel: "Submit",
buttonsEnabled: $allowToReact) {
if let sdPassword = kcSelfDestructPassword.get(), authRequest.selfDestruct && password == sdPassword {
allowToReact = false
deleteStorageAndRestart(sdPassword) { r in
m.laRequest = nil
authRequest.completed(r)
}
return
}
let r: LAResult = password == authRequest.password
? .success
: .failed(authError: NSLocalizedString("Incorrect passcode", comment: "PIN entry"))
let r: LAResult
if password == authRequest.password {
if authRequest.selfDestruct && kcSelfDestructPassword.get() != nil && !m.chatInitialized {
initChatAndMigrate()
}
r = .success
} else {
r = .failed(authError: NSLocalizedString("Incorrect passcode", comment: "PIN entry"))
}
m.laRequest = nil
authRequest.completed(r)
} cancel: {
@@ -37,8 +46,27 @@ struct LocalAuthView: View {
private func deleteStorageAndRestart(_ password: String, completed: @escaping (LAResult) -> Void) {
Task {
do {
try await stopChatAsync()
try await deleteChatAsync()
/** Waiting until [initializeChat] finishes */
while (m.ctrlInitInProgress) {
try await Task.sleep(nanoseconds: 50_000000)
}
if m.chatRunning == true {
try await stopChatAsync()
}
if m.chatInitialized {
/**
* The following sequence can bring a user here:
* the user opened the app, entered app passcode, went to background, returned back, entered self-destruct code.
* In this case database should be closed to prevent possible situation when OS can deny database removal command
* */
chatCloseStore()
}
deleteAppDatabaseAndFiles()
// Clear sensitive data on screen just in case app fails to hide its views while new database is created
m.chatId = nil
m.reversedChatItems = []
m.chats = []
m.users = []
_ = kcAppPassword.set(password)
_ = kcSelfDestructPassword.remove()
await NtfManager.shared.removeAllNotifications()
@@ -52,8 +80,8 @@ struct LocalAuthView: View {
resetChatCtrl()
try initializeChat(start: true)
m.chatDbChanged = false
appStateGroupDefault.set(.active)
if m.currentUser != nil { return }
AppChatState.shared.set(.active)
if m.currentUser != nil || !m.chatInitialized { return }
var profile: Profile? = nil
if let displayName = displayName, displayName != "" {
profile = Profile(displayName: displayName, fullName: "")

View File

@@ -14,6 +14,8 @@ struct PasscodeView: View {
var reason: String? = nil
var submitLabel: LocalizedStringKey
var submitEnabled: ((String) -> Bool)?
@Binding var buttonsEnabled: Bool
var submit: () -> Void
var cancel: () -> Void
@@ -70,11 +72,11 @@ struct PasscodeView: View {
@ViewBuilder private func buttonsView() -> some View {
Button(action: cancel) {
Label("Cancel", systemImage: "multiply")
}
}.disabled(!buttonsEnabled)
Button(action: submit) {
Label(submitLabel, systemImage: "checkmark")
}
.disabled(submitEnabled?(passcode) == false || passcode.count < 4)
.disabled(submitEnabled?(passcode) == false || passcode.count < 4 || !buttonsEnabled)
}
}
@@ -85,6 +87,7 @@ struct PasscodeViewView_Previews: PreviewProvider {
title: "Enter Passcode",
reason: "Unlock app",
submitLabel: "Submit",
buttonsEnabled: Binding.constant(true),
submit: {},
cancel: {}
)

View File

@@ -11,6 +11,7 @@ import SimpleXChat
struct SetAppPasscodeView: View {
var passcodeKeychain: KeyChainItem = kcAppPassword
var prohibitedPasscodeKeychain: KeyChainItem = kcSelfDestructPassword
var title: LocalizedStringKey = "New Passcode"
var reason: String?
var submit: () -> Void
@@ -41,7 +42,10 @@ struct SetAppPasscodeView: View {
}
}
} else {
setPasswordView(title: title, submitLabel: "Save") {
setPasswordView(title: title,
submitLabel: "Save",
// Do not allow to set app passcode == selfDestruct passcode
submitEnabled: { pwd in pwd != prohibitedPasscodeKeychain.get() }) {
enteredPassword = passcode
passcode = ""
confirming = true
@@ -54,7 +58,7 @@ struct SetAppPasscodeView: View {
}
private func setPasswordView(title: LocalizedStringKey, submitLabel: LocalizedStringKey, submitEnabled: (((String) -> Bool))? = nil, submit: @escaping () -> Void) -> some View {
PasscodeView(passcode: $passcode, title: title, reason: reason, submitLabel: submitLabel, submitEnabled: submitEnabled, submit: submit) {
PasscodeView(passcode: $passcode, title: title, reason: reason, submitLabel: submitLabel, submitEnabled: submitEnabled, buttonsEnabled: Binding.constant(true), submit: submit) {
dismiss()
cancel()
}

View File

@@ -9,8 +9,20 @@
import SwiftUI
struct AddContactLearnMore: View {
var showTitle: Bool
var body: some View {
List {
if showTitle {
Text("One-time invitation link")
.font(.largeTitle)
.bold()
.fixedSize(horizontal: false, vertical: true)
.padding(.vertical)
.listRowBackground(Color.clear)
.listRowSeparator(.hidden)
.listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
}
VStack(alignment: .leading, spacing: 18) {
Text("To connect, your contact can scan QR code or use the link in the app.")
Text("If you can't meet in person, show QR code in a video call, or share the link.")
@@ -23,6 +35,6 @@ struct AddContactLearnMore: View {
struct AddContactLearnMore_Previews: PreviewProvider {
static var previews: some View {
AddContactLearnMore()
AddContactLearnMore(showTitle: true)
}
}

View File

@@ -1,129 +0,0 @@
//
// AddContactView.swift
// SimpleX
//
// Created by Evgeny Poberezkin on 29/01/2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
import SwiftUI
import CoreImage.CIFilterBuiltins
import SimpleXChat
struct AddContactView: View {
@EnvironmentObject private var chatModel: ChatModel
@Binding var contactConnection: PendingContactConnection?
var connReqInvitation: String
@AppStorage(GROUP_DEFAULT_INCOGNITO, store: groupDefaults) private var incognitoDefault = false
var body: some View {
VStack {
List {
Section {
if connReqInvitation != "" {
SimpleXLinkQRCode(uri: connReqInvitation)
} else {
ProgressView()
.progressViewStyle(.circular)
.scaleEffect(2)
.frame(maxWidth: .infinity)
.padding(.vertical)
}
IncognitoToggle(incognitoEnabled: $incognitoDefault)
.disabled(contactConnection == nil)
shareLinkButton(connReqInvitation)
oneTimeLinkLearnMoreButton()
} header: {
Text("1-time link")
} footer: {
sharedProfileInfo(incognitoDefault)
}
}
}
.onAppear { chatModel.connReqInv = connReqInvitation }
.onChange(of: incognitoDefault) { incognito in
Task {
do {
if let contactConn = contactConnection,
let conn = try await apiSetConnectionIncognito(connId: contactConn.pccConnId, incognito: incognito) {
await MainActor.run {
contactConnection = conn
chatModel.updateContactConnection(conn)
}
}
} catch {
logger.error("apiSetConnectionIncognito error: \(responseError(error))")
}
}
}
}
}
struct IncognitoToggle: View {
@Binding var incognitoEnabled: Bool
@State private var showIncognitoSheet = false
var body: some View {
ZStack(alignment: .leading) {
Image(systemName: incognitoEnabled ? "theatermasks.fill" : "theatermasks")
.frame(maxWidth: 24, maxHeight: 24, alignment: .center)
.foregroundColor(incognitoEnabled ? Color.indigo : .secondary)
.font(.system(size: 14))
Toggle(isOn: $incognitoEnabled) {
HStack(spacing: 6) {
Text("Incognito")
Image(systemName: "info.circle")
.foregroundColor(.accentColor)
.font(.system(size: 14))
}
.onTapGesture {
showIncognitoSheet = true
}
}
.padding(.leading, 36)
}
.sheet(isPresented: $showIncognitoSheet) {
IncognitoHelp()
}
}
}
func sharedProfileInfo(_ incognito: Bool) -> Text {
let name = ChatModel.shared.currentUser?.displayName ?? ""
return Text(
incognito
? "A new random profile will be shared."
: "Your profile **\(name)** will be shared."
)
}
func shareLinkButton(_ connReqInvitation: String) -> some View {
Button {
showShareSheet(items: [simplexChatLink(connReqInvitation)])
} label: {
settingsRow("square.and.arrow.up") {
Text("Share 1-time link")
}
}
}
func oneTimeLinkLearnMoreButton() -> some View {
NavigationLink {
AddContactLearnMore()
.navigationTitle("One-time invitation link")
.navigationBarTitleDisplayMode(.large)
} label: {
settingsRow("info.circle") {
Text("Learn more")
}
}
}
struct AddContactView_Previews: PreviewProvider {
static var previews: some View {
AddContactView(
contactConnection: Binding.constant(PendingContactConnection.getSampleData()),
connReqInvitation: "https://simplex.chat/invitation#/?v=1&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FFe5ICmvrm4wkrr6X1LTMii-lhBqLeB76%23MCowBQYDK2VuAyEAdhZZsHpuaAk3Hh1q0uNb_6hGTpuwBIrsp2z9U2T0oC0%3D&e2e=v%3D1%26x3dh%3DMEIwBQYDK2VvAzkAcz6jJk71InuxA0bOX7OUhddfB8Ov7xwQIlIDeXBRZaOntUU4brU5Y3rBzroZBdQJi0FKdtt_D7I%3D%2CMEIwBQYDK2VvAzkA-hDvk1duBi1hlOr08VWSI-Ou4JNNSQjseY69QyKm7Kgg1zZjbpGfyBqSZ2eqys6xtoV4ZtoQUXQ%3D"
)
}
}

View File

@@ -130,8 +130,10 @@ struct AddGroupView: View {
}
}
.sheet(isPresented: $showImagePicker) {
LibraryImagePicker(image: $chosenImage) {
didSelectItem in showImagePicker = false
LibraryImagePicker(image: $chosenImage) { _ in
await MainActor.run {
showImagePicker = false
}
}
}
.alert(isPresented: $showInvalidNameAlert) {
@@ -185,6 +187,7 @@ struct AddGroupView: View {
hideKeyboard()
do {
profile.displayName = profile.displayName.trimmingCharacters(in: .whitespaces)
profile.groupPreferences = GroupPreferences(history: GroupPreference(enable: .on))
let gInfo = try apiNewGroup(incognito: incognitoDefault, groupProfile: profile)
Task {
let groupMembers = await apiListMembers(gInfo.groupId)

View File

@@ -1,42 +0,0 @@
//
// ConnectViaLinkView.swift
// SimpleX (iOS)
//
// Created by Evgeny on 21/09/2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
import SwiftUI
enum ConnectViaLinkTab: String {
case scan
case paste
}
struct ConnectViaLinkView: View {
@State private var selection: ConnectViaLinkTab = connectViaLinkTabDefault.get()
var body: some View {
TabView(selection: $selection) {
ScanToConnectView()
.tabItem {
Label("Scan QR code", systemImage: "qrcode")
}
.tag(ConnectViaLinkTab.scan)
PasteToConnectView()
.tabItem {
Label("Paste received link", systemImage: "doc.plaintext")
}
.tag(ConnectViaLinkTab.paste)
}
.onChange(of: selection) { _ in
connectViaLinkTabDefault.set(selection)
}
}
}
struct ConnectViaLinkView_Previews: PreviewProvider {
static var previews: some View {
ConnectViaLinkView()
}
}

View File

@@ -1,93 +0,0 @@
//
// CreateLinkView.swift
// SimpleX (iOS)
//
// Created by Evgeny on 21/09/2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
import SwiftUI
import SimpleXChat
enum CreateLinkTab {
case oneTime
case longTerm
var title: LocalizedStringKey {
switch self {
case .oneTime: return "One-time invitation link"
case .longTerm: return "Your SimpleX address"
}
}
}
struct CreateLinkView: View {
@EnvironmentObject var m: ChatModel
@State var selection: CreateLinkTab
@State var connReqInvitation: String = ""
@State var contactConnection: PendingContactConnection? = nil
@State private var creatingConnReq = false
var viaNavLink = false
var body: some View {
if viaNavLink {
createLinkView()
} else {
NavigationView {
createLinkView()
}
}
}
private func createLinkView() -> some View {
TabView(selection: $selection) {
AddContactView(contactConnection: $contactConnection, connReqInvitation: connReqInvitation)
.tabItem {
Label(
connReqInvitation == ""
? "Create one-time invitation link"
: "One-time invitation link",
systemImage: "1.circle"
)
}
.tag(CreateLinkTab.oneTime)
UserAddressView(viaCreateLinkView: true)
.tabItem {
Label("Your SimpleX address", systemImage: "infinity.circle")
}
.tag(CreateLinkTab.longTerm)
}
.onChange(of: selection) { _ in
if case .oneTime = selection, connReqInvitation == "", contactConnection == nil && !creatingConnReq {
createInvitation()
}
}
.onAppear { m.connReqInv = connReqInvitation }
.onDisappear { m.connReqInv = nil }
.navigationTitle(selection.title)
.navigationBarTitleDisplayMode(.large)
}
private func createInvitation() {
creatingConnReq = true
Task {
if let (connReq, pcc) = await apiAddContact(incognito: incognitoGroupDefault.get()) {
await MainActor.run {
connReqInvitation = connReq
contactConnection = pcc
m.connReqInv = connReq
}
} else {
await MainActor.run {
creatingConnReq = false
}
}
}
}
}
struct CreateLinkView_Previews: PreviewProvider {
static var previews: some View {
CreateLinkView(selection: CreateLinkTab.oneTime)
}
}

View File

@@ -1,460 +0,0 @@
//
// NewChatButton.swift
// SimpleX
//
// Created by Evgeny Poberezkin on 31/01/2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
import SwiftUI
import SimpleXChat
enum NewChatAction: Identifiable {
case createLink(link: String, connection: PendingContactConnection)
case connectViaLink
case createGroup
var id: String {
switch self {
case let .createLink(link, _): return "createLink \(link)"
case .connectViaLink: return "connectViaLink"
case .createGroup: return "createGroup"
}
}
}
struct NewChatButton: View {
@Binding var showAddChat: Bool
@State private var actionSheet: NewChatAction?
var body: some View {
Button { showAddChat = true } label: {
Image(systemName: "square.and.pencil")
.resizable()
.scaledToFit()
.frame(width: 24, height: 24)
}
.confirmationDialog("Start a new chat", isPresented: $showAddChat, titleVisibility: .visible) {
Button("Share one-time invitation link") { addContactAction() }
Button("Connect via link / QR code") { actionSheet = .connectViaLink }
Button("Create secret group") { actionSheet = .createGroup }
}
.sheet(item: $actionSheet) { sheet in
switch sheet {
case let .createLink(link, pcc):
CreateLinkView(selection: .oneTime, connReqInvitation: link, contactConnection: pcc)
case .connectViaLink: ConnectViaLinkView()
case .createGroup: AddGroupView()
}
}
}
func addContactAction() {
Task {
if let (connReq, pcc) = await apiAddContact(incognito: incognitoGroupDefault.get()) {
actionSheet = .createLink(link: connReq, connection: pcc)
}
}
}
}
enum PlanAndConnectAlert: Identifiable {
case ownInvitationLinkConfirmConnect(connectionLink: String, connectionPlan: ConnectionPlan, incognito: Bool)
case invitationLinkConnecting(connectionLink: String)
case ownContactAddressConfirmConnect(connectionLink: String, connectionPlan: ConnectionPlan, incognito: Bool)
case contactAddressConnectingConfirmReconnect(connectionLink: String, connectionPlan: ConnectionPlan, incognito: Bool)
case groupLinkConfirmConnect(connectionLink: String, connectionPlan: ConnectionPlan, incognito: Bool)
case groupLinkConnectingConfirmReconnect(connectionLink: String, connectionPlan: ConnectionPlan, incognito: Bool)
case groupLinkConnecting(connectionLink: String, groupInfo: GroupInfo?)
var id: String {
switch self {
case let .ownInvitationLinkConfirmConnect(connectionLink, _, _): return "ownInvitationLinkConfirmConnect \(connectionLink)"
case let .invitationLinkConnecting(connectionLink): return "invitationLinkConnecting \(connectionLink)"
case let .ownContactAddressConfirmConnect(connectionLink, _, _): return "ownContactAddressConfirmConnect \(connectionLink)"
case let .contactAddressConnectingConfirmReconnect(connectionLink, _, _): return "contactAddressConnectingConfirmReconnect \(connectionLink)"
case let .groupLinkConfirmConnect(connectionLink, _, _): return "groupLinkConfirmConnect \(connectionLink)"
case let .groupLinkConnectingConfirmReconnect(connectionLink, _, _): return "groupLinkConnectingConfirmReconnect \(connectionLink)"
case let .groupLinkConnecting(connectionLink, _): return "groupLinkConnecting \(connectionLink)"
}
}
}
func planAndConnectAlert(_ alert: PlanAndConnectAlert, dismiss: Bool) -> Alert {
switch alert {
case let .ownInvitationLinkConfirmConnect(connectionLink, connectionPlan, incognito):
return Alert(
title: Text("Connect to yourself?"),
message: Text("This is your own one-time link!"),
primaryButton: .destructive(
Text(incognito ? "Connect incognito" : "Connect"),
action: { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: incognito) }
),
secondaryButton: .cancel()
)
case .invitationLinkConnecting:
return Alert(
title: Text("Already connecting!"),
message: Text("You are already connecting via this one-time link!")
)
case let .ownContactAddressConfirmConnect(connectionLink, connectionPlan, incognito):
return Alert(
title: Text("Connect to yourself?"),
message: Text("This is your own SimpleX address!"),
primaryButton: .destructive(
Text(incognito ? "Connect incognito" : "Connect"),
action: { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: incognito) }
),
secondaryButton: .cancel()
)
case let .contactAddressConnectingConfirmReconnect(connectionLink, connectionPlan, incognito):
return Alert(
title: Text("Repeat connection request?"),
message: Text("You have already requested connection via this address!"),
primaryButton: .destructive(
Text(incognito ? "Connect incognito" : "Connect"),
action: { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: incognito) }
),
secondaryButton: .cancel()
)
case let .groupLinkConfirmConnect(connectionLink, connectionPlan, incognito):
return Alert(
title: Text("Join group?"),
message: Text("You will connect to all group members."),
primaryButton: .default(
Text(incognito ? "Join incognito" : "Join"),
action: { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: incognito) }
),
secondaryButton: .cancel()
)
case let .groupLinkConnectingConfirmReconnect(connectionLink, connectionPlan, incognito):
return Alert(
title: Text("Repeat join request?"),
message: Text("You are already joining the group via this link!"),
primaryButton: .destructive(
Text(incognito ? "Join incognito" : "Join"),
action: { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: incognito) }
),
secondaryButton: .cancel()
)
case let .groupLinkConnecting(_, groupInfo):
if let groupInfo = groupInfo {
return Alert(
title: Text("Group already exists!"),
message: Text("You are already joining the group \(groupInfo.displayName).")
)
} else {
return Alert(
title: Text("Already joining the group!"),
message: Text("You are already joining the group via this link.")
)
}
}
}
enum PlanAndConnectActionSheet: Identifiable {
case askCurrentOrIncognitoProfile(connectionLink: String, connectionPlan: ConnectionPlan?, title: LocalizedStringKey)
case askCurrentOrIncognitoProfileDestructive(connectionLink: String, connectionPlan: ConnectionPlan, title: LocalizedStringKey)
case askCurrentOrIncognitoProfileConnectContactViaAddress(contact: Contact)
case ownGroupLinkConfirmConnect(connectionLink: String, connectionPlan: ConnectionPlan, incognito: Bool?, groupInfo: GroupInfo)
var id: String {
switch self {
case let .askCurrentOrIncognitoProfile(connectionLink, _, _): return "askCurrentOrIncognitoProfile \(connectionLink)"
case let .askCurrentOrIncognitoProfileDestructive(connectionLink, _, _): return "askCurrentOrIncognitoProfileDestructive \(connectionLink)"
case let .askCurrentOrIncognitoProfileConnectContactViaAddress(contact): return "askCurrentOrIncognitoProfileConnectContactViaAddress \(contact.contactId)"
case let .ownGroupLinkConfirmConnect(connectionLink, _, _, _): return "ownGroupLinkConfirmConnect \(connectionLink)"
}
}
}
func planAndConnectActionSheet(_ sheet: PlanAndConnectActionSheet, dismiss: Bool) -> ActionSheet {
switch sheet {
case let .askCurrentOrIncognitoProfile(connectionLink, connectionPlan, title):
return ActionSheet(
title: Text(title),
buttons: [
.default(Text("Use current profile")) { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: false) },
.default(Text("Use new incognito profile")) { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: true) },
.cancel()
]
)
case let .askCurrentOrIncognitoProfileDestructive(connectionLink, connectionPlan, title):
return ActionSheet(
title: Text(title),
buttons: [
.destructive(Text("Use current profile")) { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: false) },
.destructive(Text("Use new incognito profile")) { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: true) },
.cancel()
]
)
case let .askCurrentOrIncognitoProfileConnectContactViaAddress(contact):
return ActionSheet(
title: Text("Connect with \(contact.chatViewName)"),
buttons: [
.default(Text("Use current profile")) { connectContactViaAddress_(contact, dismiss: dismiss, incognito: false) },
.default(Text("Use new incognito profile")) { connectContactViaAddress_(contact, dismiss: dismiss, incognito: true) },
.cancel()
]
)
case let .ownGroupLinkConfirmConnect(connectionLink, connectionPlan, incognito, groupInfo):
if let incognito = incognito {
return ActionSheet(
title: Text("Join your group?\nThis is your link for group \(groupInfo.displayName)!"),
buttons: [
.default(Text("Open group")) { openKnownGroup(groupInfo, dismiss: dismiss, showAlreadyExistsAlert: nil) },
.destructive(Text(incognito ? "Join incognito" : "Join with current profile")) { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: incognito) },
.cancel()
]
)
} else {
return ActionSheet(
title: Text("Join your group?\nThis is your link for group \(groupInfo.displayName)!"),
buttons: [
.default(Text("Open group")) { openKnownGroup(groupInfo, dismiss: dismiss, showAlreadyExistsAlert: nil) },
.destructive(Text("Use current profile")) { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: false) },
.destructive(Text("Use new incognito profile")) { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: true) },
.cancel()
]
)
}
}
}
func planAndConnect(
_ connectionLink: String,
showAlert: @escaping (PlanAndConnectAlert) -> Void,
showActionSheet: @escaping (PlanAndConnectActionSheet) -> Void,
dismiss: Bool,
incognito: Bool?
) {
Task {
do {
let connectionPlan = try await apiConnectPlan(connReq: connectionLink)
switch connectionPlan {
case let .invitationLink(ilp):
switch ilp {
case .ok:
logger.debug("planAndConnect, .invitationLink, .ok, incognito=\(incognito?.description ?? "nil")")
if let incognito = incognito {
connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: incognito)
} else {
showActionSheet(.askCurrentOrIncognitoProfile(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "Connect via one-time link"))
}
case .ownLink:
logger.debug("planAndConnect, .invitationLink, .ownLink, incognito=\(incognito?.description ?? "nil")")
if let incognito = incognito {
showAlert(.ownInvitationLinkConfirmConnect(connectionLink: connectionLink, connectionPlan: connectionPlan, incognito: incognito))
} else {
showActionSheet(.askCurrentOrIncognitoProfileDestructive(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "Connect to yourself?\nThis is your own one-time link!"))
}
case let .connecting(contact_):
logger.debug("planAndConnect, .invitationLink, .connecting, incognito=\(incognito?.description ?? "nil")")
if let contact = contact_ {
openKnownContact(contact, dismiss: dismiss) { AlertManager.shared.showAlert(contactAlreadyConnectingAlert(contact)) }
} else {
showAlert(.invitationLinkConnecting(connectionLink: connectionLink))
}
case let .known(contact):
logger.debug("planAndConnect, .invitationLink, .known, incognito=\(incognito?.description ?? "nil")")
openKnownContact(contact, dismiss: dismiss) { AlertManager.shared.showAlert(contactAlreadyExistsAlert(contact)) }
}
case let .contactAddress(cap):
switch cap {
case .ok:
logger.debug("planAndConnect, .contactAddress, .ok, incognito=\(incognito?.description ?? "nil")")
if let incognito = incognito {
connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: incognito)
} else {
showActionSheet(.askCurrentOrIncognitoProfile(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "Connect via contact address"))
}
case .ownLink:
logger.debug("planAndConnect, .contactAddress, .ownLink, incognito=\(incognito?.description ?? "nil")")
if let incognito = incognito {
showAlert(.ownContactAddressConfirmConnect(connectionLink: connectionLink, connectionPlan: connectionPlan, incognito: incognito))
} else {
showActionSheet(.askCurrentOrIncognitoProfileDestructive(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "Connect to yourself?\nThis is your own SimpleX address!"))
}
case .connectingConfirmReconnect:
logger.debug("planAndConnect, .contactAddress, .connectingConfirmReconnect, incognito=\(incognito?.description ?? "nil")")
if let incognito = incognito {
showAlert(.contactAddressConnectingConfirmReconnect(connectionLink: connectionLink, connectionPlan: connectionPlan, incognito: incognito))
} else {
showActionSheet(.askCurrentOrIncognitoProfileDestructive(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "You have already requested connection!\nRepeat connection request?"))
}
case let .connectingProhibit(contact):
logger.debug("planAndConnect, .contactAddress, .connectingProhibit, incognito=\(incognito?.description ?? "nil")")
openKnownContact(contact, dismiss: dismiss) { AlertManager.shared.showAlert(contactAlreadyConnectingAlert(contact)) }
case let .known(contact):
logger.debug("planAndConnect, .contactAddress, .known, incognito=\(incognito?.description ?? "nil")")
openKnownContact(contact, dismiss: dismiss) { AlertManager.shared.showAlert(contactAlreadyExistsAlert(contact)) }
case let .contactViaAddress(contact):
logger.debug("planAndConnect, .contactAddress, .contactViaAddress, incognito=\(incognito?.description ?? "nil")")
if let incognito = incognito {
connectContactViaAddress_(contact, dismiss: dismiss, incognito: incognito)
} else {
showActionSheet(.askCurrentOrIncognitoProfileConnectContactViaAddress(contact: contact))
}
}
case let .groupLink(glp):
switch glp {
case .ok:
if let incognito = incognito {
showAlert(.groupLinkConfirmConnect(connectionLink: connectionLink, connectionPlan: connectionPlan, incognito: incognito))
} else {
showActionSheet(.askCurrentOrIncognitoProfile(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "Join group"))
}
case let .ownLink(groupInfo):
logger.debug("planAndConnect, .groupLink, .ownLink, incognito=\(incognito?.description ?? "nil")")
showActionSheet(.ownGroupLinkConfirmConnect(connectionLink: connectionLink, connectionPlan: connectionPlan, incognito: incognito, groupInfo: groupInfo))
case .connectingConfirmReconnect:
logger.debug("planAndConnect, .groupLink, .connectingConfirmReconnect, incognito=\(incognito?.description ?? "nil")")
if let incognito = incognito {
showAlert(.groupLinkConnectingConfirmReconnect(connectionLink: connectionLink, connectionPlan: connectionPlan, incognito: incognito))
} else {
showActionSheet(.askCurrentOrIncognitoProfileDestructive(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "You are already joining the group!\nRepeat join request?"))
}
case let .connectingProhibit(groupInfo_):
logger.debug("planAndConnect, .groupLink, .connectingProhibit, incognito=\(incognito?.description ?? "nil")")
showAlert(.groupLinkConnecting(connectionLink: connectionLink, groupInfo: groupInfo_))
case let .known(groupInfo):
logger.debug("planAndConnect, .groupLink, .known, incognito=\(incognito?.description ?? "nil")")
openKnownGroup(groupInfo, dismiss: dismiss) { AlertManager.shared.showAlert(groupAlreadyExistsAlert(groupInfo)) }
}
}
} catch {
logger.debug("planAndConnect, plan error")
if let incognito = incognito {
connectViaLink(connectionLink, connectionPlan: nil, dismiss: dismiss, incognito: incognito)
} else {
showActionSheet(.askCurrentOrIncognitoProfile(connectionLink: connectionLink, connectionPlan: nil, title: "Connect via link"))
}
}
}
}
private func connectContactViaAddress_(_ contact: Contact, dismiss: Bool, incognito: Bool) {
Task {
if dismiss {
DispatchQueue.main.async {
dismissAllSheets(animated: true)
}
}
_ = await connectContactViaAddress(contact.contactId, incognito)
}
}
private func connectViaLink(_ connectionLink: String, connectionPlan: ConnectionPlan?, dismiss: Bool, incognito: Bool) {
Task {
if let connReqType = await apiConnect(incognito: incognito, connReq: connectionLink) {
let crt: ConnReqType
if let plan = connectionPlan {
crt = planToConnReqType(plan)
} else {
crt = connReqType
}
DispatchQueue.main.async {
if dismiss {
dismissAllSheets(animated: true) {
AlertManager.shared.showAlert(connReqSentAlert(crt))
}
} else {
AlertManager.shared.showAlert(connReqSentAlert(crt))
}
}
} else {
if dismiss {
DispatchQueue.main.async {
dismissAllSheets(animated: true)
}
}
}
}
}
func openKnownContact(_ contact: Contact, dismiss: Bool, showAlreadyExistsAlert: (() -> Void)?) {
Task {
let m = ChatModel.shared
if let c = m.getContactChat(contact.contactId) {
DispatchQueue.main.async {
if dismiss {
dismissAllSheets(animated: true) {
m.chatId = c.id
showAlreadyExistsAlert?()
}
} else {
m.chatId = c.id
showAlreadyExistsAlert?()
}
}
}
}
}
func openKnownGroup(_ groupInfo: GroupInfo, dismiss: Bool, showAlreadyExistsAlert: (() -> Void)?) {
Task {
let m = ChatModel.shared
if let g = m.getGroupChat(groupInfo.groupId) {
DispatchQueue.main.async {
if dismiss {
dismissAllSheets(animated: true) {
m.chatId = g.id
showAlreadyExistsAlert?()
}
} else {
m.chatId = g.id
showAlreadyExistsAlert?()
}
}
}
}
}
func contactAlreadyConnectingAlert(_ contact: Contact) -> Alert {
mkAlert(
title: "Contact already exists",
message: "You are already connecting to \(contact.displayName)."
)
}
func groupAlreadyExistsAlert(_ groupInfo: GroupInfo) -> Alert {
mkAlert(
title: "Group already exists",
message: "You are already in group \(groupInfo.displayName)."
)
}
enum ConnReqType: Equatable {
case invitation
case contact
case groupLink
var connReqSentText: LocalizedStringKey {
switch self {
case .invitation: return "You will be connected when your contact's device is online, please wait or check later!"
case .contact: return "You will be connected when your connection request is accepted, please wait or check later!"
case .groupLink: return "You will be connected when group link host's device is online, please wait or check later!"
}
}
}
private func planToConnReqType(_ connectionPlan: ConnectionPlan) -> ConnReqType {
switch connectionPlan {
case .invitationLink: return .invitation
case .contactAddress: return .contact
case .groupLink: return .groupLink
}
}
func connReqSentAlert(_ type: ConnReqType) -> Alert {
return mkAlert(
title: "Connection request sent!",
message: type.connReqSentText
)
}
struct NewChatButton_Previews: PreviewProvider {
static var previews: some View {
NewChatButton(showAddChat: Binding.constant(false))
}
}

View File

@@ -0,0 +1,52 @@
//
// NewChatMenuButton.swift
// SimpleX (iOS)
//
// Created by spaced4ndy on 28.11.2023.
// Copyright © 2023 SimpleX Chat. All rights reserved.
//
import SwiftUI
enum NewChatMenuOption: Identifiable {
case newContact
case newGroup
var id: Self { self }
}
struct NewChatMenuButton: View {
@Binding var newChatMenuOption: NewChatMenuOption?
var body: some View {
Menu {
Button {
newChatMenuOption = .newContact
} label: {
Text("Add contact")
}
Button {
newChatMenuOption = .newGroup
} label: {
Text("Create group")
}
} label: {
Image(systemName: "square.and.pencil")
.resizable()
.scaledToFit()
.frame(width: 24, height: 24)
}
.sheet(item: $newChatMenuOption) { opt in
switch opt {
case .newContact: NewChatView(selection: .invite)
case .newGroup: AddGroupView()
}
}
}
}
#Preview {
NewChatMenuButton(
newChatMenuOption: Binding.constant(nil)
)
}

View File

@@ -0,0 +1,959 @@
//
// NewChatView.swift
// SimpleX (iOS)
//
// Created by spaced4ndy on 28.11.2023.
// Copyright © 2023 SimpleX Chat. All rights reserved.
//
import SwiftUI
import SimpleXChat
import CodeScanner
import AVFoundation
enum SomeAlert: Identifiable {
case someAlert(alert: Alert, id: String)
var id: String {
switch self {
case let .someAlert(_, id): return id
}
}
}
private enum NewChatViewAlert: Identifiable {
case planAndConnectAlert(alert: PlanAndConnectAlert)
case newChatSomeAlert(alert: SomeAlert)
var id: String {
switch self {
case let .planAndConnectAlert(alert): return "planAndConnectAlert \(alert.id)"
case let .newChatSomeAlert(alert): return "newChatSomeAlert \(alert.id)"
}
}
}
enum NewChatOption: Identifiable {
case invite
case connect
var id: Self { self }
}
struct NewChatView: View {
@EnvironmentObject var m: ChatModel
@State var selection: NewChatOption
@State var showQRCodeScanner = false
@State private var invitationUsed: Bool = false
@State private var contactConnection: PendingContactConnection? = nil
@State private var connReqInvitation: String = ""
@State private var creatingConnReq = false
@State private var pastedLink: String = ""
@State private var alert: NewChatViewAlert?
var body: some View {
VStack(alignment: .leading) {
HStack {
Text("New chat")
.font(.largeTitle)
.bold()
.fixedSize(horizontal: false, vertical: true)
Spacer()
InfoSheetButton {
AddContactLearnMore(showTitle: true)
}
}
.padding()
.padding(.top)
Picker("New chat", selection: $selection) {
Label("Add contact", systemImage: "link")
.tag(NewChatOption.invite)
Label("Connect via link", systemImage: "qrcode")
.tag(NewChatOption.connect)
}
.pickerStyle(.segmented)
.padding()
VStack {
// it seems there's a bug in iOS 15 if several views in switch (or if-else) statement have different transitions
// https://developer.apple.com/forums/thread/714977?answerId=731615022#731615022
if case .invite = selection {
prepareAndInviteView()
.transition(.move(edge: .leading))
.onAppear {
createInvitation()
}
}
if case .connect = selection {
ConnectView(showQRCodeScanner: showQRCodeScanner, pastedLink: $pastedLink, alert: $alert)
.transition(.move(edge: .trailing))
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(
// Rectangle is needed for swipe gesture to work on mostly empty views (creatingLinkProgressView and retryButton)
Rectangle()
.fill(Color(uiColor: .systemGroupedBackground))
)
.animation(.easeInOut(duration: 0.3333), value: selection)
.gesture(DragGesture(minimumDistance: 20.0, coordinateSpace: .local)
.onChanged { value in
switch(value.translation.width, value.translation.height) {
case (...0, -30...30): // left swipe
if selection == .invite {
selection = .connect
}
case (0..., -30...30): // right swipe
if selection == .connect {
selection = .invite
}
default: ()
}
}
)
}
.background(Color(.systemGroupedBackground))
.onChange(of: invitationUsed) { used in
if used && !(m.showingInvitation?.connChatUsed ?? true) {
m.markShowingInvitationUsed()
}
}
.onDisappear {
if !(m.showingInvitation?.connChatUsed ?? true),
let conn = contactConnection {
AlertManager.shared.showAlert(Alert(
title: Text("Keep unused invitation?"),
message: Text("You can view invitation link again in connection details."),
primaryButton: .default(Text("Keep")) {},
secondaryButton: .destructive(Text("Delete")) {
Task {
await deleteChat(Chat(
chatInfo: .contactConnection(contactConnection: conn),
chatItems: []
))
}
}
))
}
m.showingInvitation = nil
}
.alert(item: $alert) { a in
switch(a) {
case let .planAndConnectAlert(alert):
return planAndConnectAlert(alert, dismiss: true, cleanup: { pastedLink = "" })
case let .newChatSomeAlert(.someAlert(alert, _)):
return alert
}
}
}
private func prepareAndInviteView() -> some View {
ZStack { // ZStack is needed for views to not make transitions between each other
if connReqInvitation != "" {
InviteView(
invitationUsed: $invitationUsed,
contactConnection: $contactConnection,
connReqInvitation: connReqInvitation
)
} else if creatingConnReq {
creatingLinkProgressView()
} else {
retryButton()
}
}
}
private func createInvitation() {
if connReqInvitation == "" && contactConnection == nil && !creatingConnReq {
creatingConnReq = true
Task {
_ = try? await Task.sleep(nanoseconds: 250_000000)
let (r, apiAlert) = await apiAddContact(incognito: incognitoGroupDefault.get())
if let (connReq, pcc) = r {
await MainActor.run {
m.updateContactConnection(pcc)
m.showingInvitation = ShowingInvitation(connId: pcc.id, connChatUsed: false)
connReqInvitation = connReq
contactConnection = pcc
}
} else {
await MainActor.run {
creatingConnReq = false
if let apiAlert = apiAlert {
alert = .newChatSomeAlert(alert: .someAlert(alert: apiAlert, id: "createInvitation error"))
}
}
}
}
}
}
// Rectangle here and in retryButton are needed for gesture to work
private func creatingLinkProgressView() -> some View {
ProgressView("Creating link…")
.progressViewStyle(.circular)
}
private func retryButton() -> some View {
Button(action: createInvitation) {
VStack(spacing: 6) {
Image(systemName: "arrow.counterclockwise")
Text("Retry")
}
}
}
}
private struct InviteView: View {
@EnvironmentObject var chatModel: ChatModel
@Binding var invitationUsed: Bool
@Binding var contactConnection: PendingContactConnection?
var connReqInvitation: String
@AppStorage(GROUP_DEFAULT_INCOGNITO, store: groupDefaults) private var incognitoDefault = false
var body: some View {
List {
Section("Share this 1-time invite link") {
shareLinkView()
}
.listRowInsets(EdgeInsets(top: 0, leading: 20, bottom: 0, trailing: 10))
qrCodeView()
Section {
IncognitoToggle(incognitoEnabled: $incognitoDefault)
} footer: {
sharedProfileInfo(incognitoDefault)
}
}
.onChange(of: incognitoDefault) { incognito in
Task {
do {
if let contactConn = contactConnection,
let conn = try await apiSetConnectionIncognito(connId: contactConn.pccConnId, incognito: incognito) {
await MainActor.run {
contactConnection = conn
chatModel.updateContactConnection(conn)
}
}
} catch {
logger.error("apiSetConnectionIncognito error: \(responseError(error))")
}
}
setInvitationUsed()
}
}
private func shareLinkView() -> some View {
HStack {
let link = simplexChatLink(connReqInvitation)
linkTextView(link)
Button {
showShareSheet(items: [link])
setInvitationUsed()
} label: {
Image(systemName: "square.and.arrow.up")
.padding(.top, -7)
}
}
.frame(maxWidth: .infinity)
}
private func qrCodeView() -> some View {
Section("Or show this code") {
SimpleXLinkQRCode(uri: connReqInvitation, onShare: setInvitationUsed)
.padding()
.background(
RoundedRectangle(cornerRadius: 12, style: .continuous)
.fill(Color(uiColor: .secondarySystemGroupedBackground))
)
.padding(.horizontal)
.listRowBackground(Color.clear)
.listRowSeparator(.hidden)
.listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
}
}
private func setInvitationUsed() {
if !invitationUsed {
invitationUsed = true
}
}
}
private struct ConnectView: View {
@Environment(\.dismiss) var dismiss: DismissAction
@State var showQRCodeScanner = false
@State private var cameraAuthorizationStatus: AVAuthorizationStatus?
@Binding var pastedLink: String
@Binding var alert: NewChatViewAlert?
@State private var sheet: PlanAndConnectActionSheet?
var body: some View {
List {
Section("Paste the link you received") {
pasteLinkView()
}
scanCodeView()
}
.actionSheet(item: $sheet) { s in
planAndConnectActionSheet(s, dismiss: true, cleanup: { pastedLink = "" })
}
.onAppear {
let status = AVCaptureDevice.authorizationStatus(for: .video)
cameraAuthorizationStatus = status
if showQRCodeScanner {
switch status {
case .notDetermined: askCameraAuthorization()
case .restricted: showQRCodeScanner = false
case .denied: showQRCodeScanner = false
case .authorized: ()
@unknown default: askCameraAuthorization()
}
}
}
}
func askCameraAuthorization(_ cb: (() -> Void)? = nil) {
AVCaptureDevice.requestAccess(for: .video) { allowed in
cameraAuthorizationStatus = AVCaptureDevice.authorizationStatus(for: .video)
if allowed { cb?() }
}
}
@ViewBuilder private func pasteLinkView() -> some View {
if pastedLink == "" {
Button {
if let str = UIPasteboard.general.string {
if let link = strHasSingleSimplexLink(str.trimmingCharacters(in: .whitespaces)) {
pastedLink = link.text
// It would be good to hide it, but right now it is not clear how to release camera in CodeScanner
// https://github.com/twostraws/CodeScanner/issues/121
// No known tricks worked (changing view ID, wrapping it in another view, etc.)
// showQRCodeScanner = false
connect(pastedLink)
} else {
alert = .newChatSomeAlert(alert: .someAlert(
alert: mkAlert(title: "Invalid link", message: "The text you pasted is not a SimpleX link."),
id: "pasteLinkView: code is not a SimpleX link"
))
}
}
} label: {
Text("Tap to paste link")
}
.disabled(!ChatModel.shared.pasteboardHasStrings)
.frame(maxWidth: .infinity, alignment: .center)
} else {
linkTextView(pastedLink)
}
}
private func scanCodeView() -> some View {
Section("Or scan QR code") {
if showQRCodeScanner, case .authorized = cameraAuthorizationStatus {
CodeScannerView(codeTypes: [.qr], scanMode: .continuous, completion: processQRCode)
.aspectRatio(1, contentMode: .fit)
.cornerRadius(12)
.listRowBackground(Color.clear)
.listRowSeparator(.hidden)
.listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
.padding(.horizontal)
} else {
Button {
switch cameraAuthorizationStatus {
case .notDetermined: askCameraAuthorization { showQRCodeScanner = true }
case .restricted: ()
case .denied: UIApplication.shared.open(appSettingsURL)
case .authorized: showQRCodeScanner = true
default: askCameraAuthorization { showQRCodeScanner = true }
}
} label: {
ZStack {
Rectangle()
.aspectRatio(contentMode: .fill)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.foregroundColor(Color.clear)
switch cameraAuthorizationStatus {
case .restricted: Text("Camera not available")
case .denied: Label("Enable camera access", systemImage: "camera")
default: Label("Tap to scan", systemImage: "qrcode")
}
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
.padding()
.background(
RoundedRectangle(cornerRadius: 12, style: .continuous)
.fill(Color(uiColor: .secondarySystemGroupedBackground))
)
.padding(.horizontal)
.listRowBackground(Color.clear)
.listRowSeparator(.hidden)
.listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
.disabled(cameraAuthorizationStatus == .restricted)
}
}
}
private func processQRCode(_ resp: Result<ScanResult, ScanError>) {
switch resp {
case let .success(r):
let link = r.string
if strIsSimplexLink(r.string) {
connect(link)
} else {
alert = .newChatSomeAlert(alert: .someAlert(
alert: mkAlert(title: "Invalid QR code", message: "The code you scanned is not a SimpleX link QR code."),
id: "processQRCode: code is not a SimpleX link"
))
}
case let .failure(e):
logger.error("processQRCode QR code error: \(e.localizedDescription)")
alert = .newChatSomeAlert(alert: .someAlert(
alert: mkAlert(title: "Invalid QR code", message: "Error scanning code: \(e.localizedDescription)"),
id: "processQRCode: failure"
))
}
}
private func connect(_ link: String) {
planAndConnect(
link,
showAlert: { alert = .planAndConnectAlert(alert: $0) },
showActionSheet: { sheet = $0 },
dismiss: true,
incognito: nil
)
}
}
private func linkTextView(_ link: String) -> some View {
Text(link)
.lineLimit(1)
.font(.caption)
.truncationMode(.middle)
}
struct InfoSheetButton<Content: View>: View {
@ViewBuilder let content: Content
@State private var showInfoSheet = false
var body: some View {
Button {
showInfoSheet = true
} label: {
Image(systemName: "info.circle")
.resizable()
.scaledToFit()
.frame(width: 24, height: 24)
}
.sheet(isPresented: $showInfoSheet) {
content
}
}
}
func strIsSimplexLink(_ str: String) -> Bool {
if let parsedMd = parseSimpleXMarkdown(str),
parsedMd.count == 1,
case .simplexLink = parsedMd[0].format {
return true
} else {
return false
}
}
func strHasSingleSimplexLink(_ str: String) -> FormattedText? {
if let parsedMd = parseSimpleXMarkdown(str) {
let parsedLinks = parsedMd.filter({ $0.format?.isSimplexLink ?? false })
if parsedLinks.count == 1 {
return parsedLinks[0]
} else {
return nil
}
} else {
return nil
}
}
struct IncognitoToggle: View {
@Binding var incognitoEnabled: Bool
@State private var showIncognitoSheet = false
var body: some View {
ZStack(alignment: .leading) {
Image(systemName: incognitoEnabled ? "theatermasks.fill" : "theatermasks")
.frame(maxWidth: 24, maxHeight: 24, alignment: .center)
.foregroundColor(incognitoEnabled ? Color.indigo : .secondary)
.font(.system(size: 14))
Toggle(isOn: $incognitoEnabled) {
HStack(spacing: 6) {
Text("Incognito")
Image(systemName: "info.circle")
.foregroundColor(.accentColor)
.font(.system(size: 14))
}
.onTapGesture {
showIncognitoSheet = true
}
}
.padding(.leading, 36)
}
.sheet(isPresented: $showIncognitoSheet) {
IncognitoHelp()
}
}
}
func sharedProfileInfo(_ incognito: Bool) -> Text {
let name = ChatModel.shared.currentUser?.displayName ?? ""
return Text(
incognito
? "A new random profile will be shared."
: "Your profile **\(name)** will be shared."
)
}
enum PlanAndConnectAlert: Identifiable {
case ownInvitationLinkConfirmConnect(connectionLink: String, connectionPlan: ConnectionPlan, incognito: Bool)
case invitationLinkConnecting(connectionLink: String)
case ownContactAddressConfirmConnect(connectionLink: String, connectionPlan: ConnectionPlan, incognito: Bool)
case contactAddressConnectingConfirmReconnect(connectionLink: String, connectionPlan: ConnectionPlan, incognito: Bool)
case groupLinkConfirmConnect(connectionLink: String, connectionPlan: ConnectionPlan, incognito: Bool)
case groupLinkConnectingConfirmReconnect(connectionLink: String, connectionPlan: ConnectionPlan, incognito: Bool)
case groupLinkConnecting(connectionLink: String, groupInfo: GroupInfo?)
var id: String {
switch self {
case let .ownInvitationLinkConfirmConnect(connectionLink, _, _): return "ownInvitationLinkConfirmConnect \(connectionLink)"
case let .invitationLinkConnecting(connectionLink): return "invitationLinkConnecting \(connectionLink)"
case let .ownContactAddressConfirmConnect(connectionLink, _, _): return "ownContactAddressConfirmConnect \(connectionLink)"
case let .contactAddressConnectingConfirmReconnect(connectionLink, _, _): return "contactAddressConnectingConfirmReconnect \(connectionLink)"
case let .groupLinkConfirmConnect(connectionLink, _, _): return "groupLinkConfirmConnect \(connectionLink)"
case let .groupLinkConnectingConfirmReconnect(connectionLink, _, _): return "groupLinkConnectingConfirmReconnect \(connectionLink)"
case let .groupLinkConnecting(connectionLink, _): return "groupLinkConnecting \(connectionLink)"
}
}
}
func planAndConnectAlert(_ alert: PlanAndConnectAlert, dismiss: Bool, cleanup: (() -> Void)? = nil) -> Alert {
switch alert {
case let .ownInvitationLinkConfirmConnect(connectionLink, connectionPlan, incognito):
return Alert(
title: Text("Connect to yourself?"),
message: Text("This is your own one-time link!"),
primaryButton: .destructive(
Text(incognito ? "Connect incognito" : "Connect"),
action: { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: incognito, cleanup: cleanup) }
),
secondaryButton: .cancel() { cleanup?() }
)
case .invitationLinkConnecting:
return Alert(
title: Text("Already connecting!"),
message: Text("You are already connecting via this one-time link!"),
dismissButton: .default(Text("OK")) { cleanup?() }
)
case let .ownContactAddressConfirmConnect(connectionLink, connectionPlan, incognito):
return Alert(
title: Text("Connect to yourself?"),
message: Text("This is your own SimpleX address!"),
primaryButton: .destructive(
Text(incognito ? "Connect incognito" : "Connect"),
action: { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: incognito, cleanup: cleanup) }
),
secondaryButton: .cancel() { cleanup?() }
)
case let .contactAddressConnectingConfirmReconnect(connectionLink, connectionPlan, incognito):
return Alert(
title: Text("Repeat connection request?"),
message: Text("You have already requested connection via this address!"),
primaryButton: .destructive(
Text(incognito ? "Connect incognito" : "Connect"),
action: { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: incognito, cleanup: cleanup) }
),
secondaryButton: .cancel() { cleanup?() }
)
case let .groupLinkConfirmConnect(connectionLink, connectionPlan, incognito):
return Alert(
title: Text("Join group?"),
message: Text("You will connect to all group members."),
primaryButton: .default(
Text(incognito ? "Join incognito" : "Join"),
action: { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: incognito, cleanup: cleanup) }
),
secondaryButton: .cancel() { cleanup?() }
)
case let .groupLinkConnectingConfirmReconnect(connectionLink, connectionPlan, incognito):
return Alert(
title: Text("Repeat join request?"),
message: Text("You are already joining the group via this link!"),
primaryButton: .destructive(
Text(incognito ? "Join incognito" : "Join"),
action: { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: incognito, cleanup: cleanup) }
),
secondaryButton: .cancel() { cleanup?() }
)
case let .groupLinkConnecting(_, groupInfo):
if let groupInfo = groupInfo {
return Alert(
title: Text("Group already exists!"),
message: Text("You are already joining the group \(groupInfo.displayName)."),
dismissButton: .default(Text("OK")) { cleanup?() }
)
} else {
return Alert(
title: Text("Already joining the group!"),
message: Text("You are already joining the group via this link."),
dismissButton: .default(Text("OK")) { cleanup?() }
)
}
}
}
enum PlanAndConnectActionSheet: Identifiable {
case askCurrentOrIncognitoProfile(connectionLink: String, connectionPlan: ConnectionPlan?, title: LocalizedStringKey)
case askCurrentOrIncognitoProfileDestructive(connectionLink: String, connectionPlan: ConnectionPlan, title: LocalizedStringKey)
case askCurrentOrIncognitoProfileConnectContactViaAddress(contact: Contact)
case ownGroupLinkConfirmConnect(connectionLink: String, connectionPlan: ConnectionPlan, incognito: Bool?, groupInfo: GroupInfo)
var id: String {
switch self {
case let .askCurrentOrIncognitoProfile(connectionLink, _, _): return "askCurrentOrIncognitoProfile \(connectionLink)"
case let .askCurrentOrIncognitoProfileDestructive(connectionLink, _, _): return "askCurrentOrIncognitoProfileDestructive \(connectionLink)"
case let .askCurrentOrIncognitoProfileConnectContactViaAddress(contact): return "askCurrentOrIncognitoProfileConnectContactViaAddress \(contact.contactId)"
case let .ownGroupLinkConfirmConnect(connectionLink, _, _, _): return "ownGroupLinkConfirmConnect \(connectionLink)"
}
}
}
func planAndConnectActionSheet(_ sheet: PlanAndConnectActionSheet, dismiss: Bool, cleanup: (() -> Void)? = nil) -> ActionSheet {
switch sheet {
case let .askCurrentOrIncognitoProfile(connectionLink, connectionPlan, title):
return ActionSheet(
title: Text(title),
buttons: [
.default(Text("Use current profile")) { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: false, cleanup: cleanup) },
.default(Text("Use new incognito profile")) { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: true, cleanup: cleanup) },
.cancel() { cleanup?() }
]
)
case let .askCurrentOrIncognitoProfileDestructive(connectionLink, connectionPlan, title):
return ActionSheet(
title: Text(title),
buttons: [
.destructive(Text("Use current profile")) { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: false, cleanup: cleanup) },
.destructive(Text("Use new incognito profile")) { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: true, cleanup: cleanup) },
.cancel() { cleanup?() }
]
)
case let .askCurrentOrIncognitoProfileConnectContactViaAddress(contact):
return ActionSheet(
title: Text("Connect with \(contact.chatViewName)"),
buttons: [
.default(Text("Use current profile")) { connectContactViaAddress_(contact, dismiss: dismiss, incognito: false, cleanup: cleanup) },
.default(Text("Use new incognito profile")) { connectContactViaAddress_(contact, dismiss: dismiss, incognito: true, cleanup: cleanup) },
.cancel() { cleanup?() }
]
)
case let .ownGroupLinkConfirmConnect(connectionLink, connectionPlan, incognito, groupInfo):
if let incognito = incognito {
return ActionSheet(
title: Text("Join your group?\nThis is your link for group \(groupInfo.displayName)!"),
buttons: [
.default(Text("Open group")) { openKnownGroup(groupInfo, dismiss: dismiss, showAlreadyExistsAlert: nil) },
.destructive(Text(incognito ? "Join incognito" : "Join with current profile")) { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: incognito, cleanup: cleanup) },
.cancel() { cleanup?() }
]
)
} else {
return ActionSheet(
title: Text("Join your group?\nThis is your link for group \(groupInfo.displayName)!"),
buttons: [
.default(Text("Open group")) { openKnownGroup(groupInfo, dismiss: dismiss, showAlreadyExistsAlert: nil) },
.destructive(Text("Use current profile")) { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: false, cleanup: cleanup) },
.destructive(Text("Use new incognito profile")) { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: true, cleanup: cleanup) },
.cancel() { cleanup?() }
]
)
}
}
}
func planAndConnect(
_ connectionLink: String,
showAlert: @escaping (PlanAndConnectAlert) -> Void,
showActionSheet: @escaping (PlanAndConnectActionSheet) -> Void,
dismiss: Bool,
incognito: Bool?,
cleanup: (() -> Void)? = nil,
filterKnownContact: ((Contact) -> Void)? = nil,
filterKnownGroup: ((GroupInfo) -> Void)? = nil
) {
Task {
do {
let connectionPlan = try await apiConnectPlan(connReq: connectionLink)
switch connectionPlan {
case let .invitationLink(ilp):
switch ilp {
case .ok:
logger.debug("planAndConnect, .invitationLink, .ok, incognito=\(incognito?.description ?? "nil")")
if let incognito = incognito {
connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: incognito, cleanup: cleanup)
} else {
showActionSheet(.askCurrentOrIncognitoProfile(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "Connect via one-time link"))
}
case .ownLink:
logger.debug("planAndConnect, .invitationLink, .ownLink, incognito=\(incognito?.description ?? "nil")")
if let incognito = incognito {
showAlert(.ownInvitationLinkConfirmConnect(connectionLink: connectionLink, connectionPlan: connectionPlan, incognito: incognito))
} else {
showActionSheet(.askCurrentOrIncognitoProfileDestructive(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "Connect to yourself?\nThis is your own one-time link!"))
}
case let .connecting(contact_):
logger.debug("planAndConnect, .invitationLink, .connecting, incognito=\(incognito?.description ?? "nil")")
if let contact = contact_ {
if let f = filterKnownContact {
f(contact)
} else {
openKnownContact(contact, dismiss: dismiss) { AlertManager.shared.showAlert(contactAlreadyConnectingAlert(contact)) }
}
} else {
showAlert(.invitationLinkConnecting(connectionLink: connectionLink))
}
case let .known(contact):
logger.debug("planAndConnect, .invitationLink, .known, incognito=\(incognito?.description ?? "nil")")
if let f = filterKnownContact {
f(contact)
} else {
openKnownContact(contact, dismiss: dismiss) { AlertManager.shared.showAlert(contactAlreadyExistsAlert(contact)) }
}
}
case let .contactAddress(cap):
switch cap {
case .ok:
logger.debug("planAndConnect, .contactAddress, .ok, incognito=\(incognito?.description ?? "nil")")
if let incognito = incognito {
connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: incognito, cleanup: cleanup)
} else {
showActionSheet(.askCurrentOrIncognitoProfile(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "Connect via contact address"))
}
case .ownLink:
logger.debug("planAndConnect, .contactAddress, .ownLink, incognito=\(incognito?.description ?? "nil")")
if let incognito = incognito {
showAlert(.ownContactAddressConfirmConnect(connectionLink: connectionLink, connectionPlan: connectionPlan, incognito: incognito))
} else {
showActionSheet(.askCurrentOrIncognitoProfileDestructive(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "Connect to yourself?\nThis is your own SimpleX address!"))
}
case .connectingConfirmReconnect:
logger.debug("planAndConnect, .contactAddress, .connectingConfirmReconnect, incognito=\(incognito?.description ?? "nil")")
if let incognito = incognito {
showAlert(.contactAddressConnectingConfirmReconnect(connectionLink: connectionLink, connectionPlan: connectionPlan, incognito: incognito))
} else {
showActionSheet(.askCurrentOrIncognitoProfileDestructive(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "You have already requested connection!\nRepeat connection request?"))
}
case let .connectingProhibit(contact):
logger.debug("planAndConnect, .contactAddress, .connectingProhibit, incognito=\(incognito?.description ?? "nil")")
if let f = filterKnownContact {
f(contact)
} else {
openKnownContact(contact, dismiss: dismiss) { AlertManager.shared.showAlert(contactAlreadyConnectingAlert(contact)) }
}
case let .known(contact):
logger.debug("planAndConnect, .contactAddress, .known, incognito=\(incognito?.description ?? "nil")")
if let f = filterKnownContact {
f(contact)
} else {
openKnownContact(contact, dismiss: dismiss) { AlertManager.shared.showAlert(contactAlreadyExistsAlert(contact)) }
}
case let .contactViaAddress(contact):
logger.debug("planAndConnect, .contactAddress, .contactViaAddress, incognito=\(incognito?.description ?? "nil")")
if let incognito = incognito {
connectContactViaAddress_(contact, dismiss: dismiss, incognito: incognito, cleanup: cleanup)
} else {
showActionSheet(.askCurrentOrIncognitoProfileConnectContactViaAddress(contact: contact))
}
}
case let .groupLink(glp):
switch glp {
case .ok:
if let incognito = incognito {
showAlert(.groupLinkConfirmConnect(connectionLink: connectionLink, connectionPlan: connectionPlan, incognito: incognito))
} else {
showActionSheet(.askCurrentOrIncognitoProfile(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "Join group"))
}
case let .ownLink(groupInfo):
logger.debug("planAndConnect, .groupLink, .ownLink, incognito=\(incognito?.description ?? "nil")")
if let f = filterKnownGroup {
f(groupInfo)
}
showActionSheet(.ownGroupLinkConfirmConnect(connectionLink: connectionLink, connectionPlan: connectionPlan, incognito: incognito, groupInfo: groupInfo))
case .connectingConfirmReconnect:
logger.debug("planAndConnect, .groupLink, .connectingConfirmReconnect, incognito=\(incognito?.description ?? "nil")")
if let incognito = incognito {
showAlert(.groupLinkConnectingConfirmReconnect(connectionLink: connectionLink, connectionPlan: connectionPlan, incognito: incognito))
} else {
showActionSheet(.askCurrentOrIncognitoProfileDestructive(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "You are already joining the group!\nRepeat join request?"))
}
case let .connectingProhibit(groupInfo_):
logger.debug("planAndConnect, .groupLink, .connectingProhibit, incognito=\(incognito?.description ?? "nil")")
showAlert(.groupLinkConnecting(connectionLink: connectionLink, groupInfo: groupInfo_))
case let .known(groupInfo):
logger.debug("planAndConnect, .groupLink, .known, incognito=\(incognito?.description ?? "nil")")
if let f = filterKnownGroup {
f(groupInfo)
} else {
openKnownGroup(groupInfo, dismiss: dismiss) { AlertManager.shared.showAlert(groupAlreadyExistsAlert(groupInfo)) }
}
}
}
} catch {
logger.debug("planAndConnect, plan error")
if let incognito = incognito {
connectViaLink(connectionLink, connectionPlan: nil, dismiss: dismiss, incognito: incognito, cleanup: cleanup)
} else {
showActionSheet(.askCurrentOrIncognitoProfile(connectionLink: connectionLink, connectionPlan: nil, title: "Connect via link"))
}
}
}
}
private func connectContactViaAddress_(_ contact: Contact, dismiss: Bool, incognito: Bool, cleanup: (() -> Void)? = nil) {
Task {
if dismiss {
DispatchQueue.main.async {
dismissAllSheets(animated: true)
}
}
_ = await connectContactViaAddress(contact.contactId, incognito)
cleanup?()
}
}
private func connectViaLink(
_ connectionLink: String,
connectionPlan: ConnectionPlan?,
dismiss: Bool,
incognito: Bool,
cleanup: (() -> Void)?
) {
Task {
if let (connReqType, pcc) = await apiConnect(incognito: incognito, connReq: connectionLink) {
await MainActor.run {
ChatModel.shared.updateContactConnection(pcc)
}
let crt: ConnReqType
if let plan = connectionPlan {
crt = planToConnReqType(plan)
} else {
crt = connReqType
}
DispatchQueue.main.async {
if dismiss {
dismissAllSheets(animated: true) {
AlertManager.shared.showAlert(connReqSentAlert(crt))
}
} else {
AlertManager.shared.showAlert(connReqSentAlert(crt))
}
}
} else {
if dismiss {
DispatchQueue.main.async {
dismissAllSheets(animated: true)
}
}
}
cleanup?()
}
}
func openKnownContact(_ contact: Contact, dismiss: Bool, showAlreadyExistsAlert: (() -> Void)?) {
Task {
let m = ChatModel.shared
if let c = m.getContactChat(contact.contactId) {
DispatchQueue.main.async {
if dismiss {
dismissAllSheets(animated: true) {
m.chatId = c.id
showAlreadyExistsAlert?()
}
} else {
m.chatId = c.id
showAlreadyExistsAlert?()
}
}
}
}
}
func openKnownGroup(_ groupInfo: GroupInfo, dismiss: Bool, showAlreadyExistsAlert: (() -> Void)?) {
Task {
let m = ChatModel.shared
if let g = m.getGroupChat(groupInfo.groupId) {
DispatchQueue.main.async {
if dismiss {
dismissAllSheets(animated: true) {
m.chatId = g.id
showAlreadyExistsAlert?()
}
} else {
m.chatId = g.id
showAlreadyExistsAlert?()
}
}
}
}
}
func contactAlreadyConnectingAlert(_ contact: Contact) -> Alert {
mkAlert(
title: "Contact already exists",
message: "You are already connecting to \(contact.displayName)."
)
}
func groupAlreadyExistsAlert(_ groupInfo: GroupInfo) -> Alert {
mkAlert(
title: "Group already exists",
message: "You are already in group \(groupInfo.displayName)."
)
}
enum ConnReqType: Equatable {
case invitation
case contact
case groupLink
var connReqSentText: LocalizedStringKey {
switch self {
case .invitation: return "You will be connected when your contact's device is online, please wait or check later!"
case .contact: return "You will be connected when your connection request is accepted, please wait or check later!"
case .groupLink: return "You will be connected when group link host's device is online, please wait or check later!"
}
}
}
private func planToConnReqType(_ connectionPlan: ConnectionPlan) -> ConnReqType {
switch connectionPlan {
case .invitationLink: return .invitation
case .contactAddress: return .contact
case .groupLink: return .groupLink
}
}
func connReqSentAlert(_ type: ConnReqType) -> Alert {
return mkAlert(
title: "Connection request sent!",
message: type.connReqSentText
)
}
#Preview {
NewChatView(
selection: .invite
)
}

View File

@@ -1,106 +0,0 @@
//
// PasteToConnectView.swift
// SimpleX (iOS)
//
// Created by Ian Davies on 22/04/2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
import SwiftUI
import SimpleXChat
struct PasteToConnectView: View {
@Environment(\.dismiss) var dismiss: DismissAction
@State private var connectionLink: String = ""
@AppStorage(GROUP_DEFAULT_INCOGNITO, store: groupDefaults) private var incognitoDefault = false
@FocusState private var linkEditorFocused: Bool
@State private var alert: PlanAndConnectAlert?
@State private var sheet: PlanAndConnectActionSheet?
var body: some View {
List {
Text("Connect via link")
.font(.largeTitle)
.bold()
.fixedSize(horizontal: false, vertical: true)
.listRowBackground(Color.clear)
.listRowSeparator(.hidden)
.listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
.onTapGesture { linkEditorFocused = false }
Section {
linkEditor()
Button {
if connectionLink == "" {
connectionLink = UIPasteboard.general.string ?? ""
} else {
connectionLink = ""
}
} label: {
if connectionLink == "" {
settingsRow("doc.plaintext") { Text("Paste") }
} else {
settingsRow("multiply") { Text("Clear") }
}
}
Button {
connect()
} label: {
settingsRow("link") { Text("Connect") }
}
.disabled(connectionLink == "" || connectionLink.trimmingCharacters(in: .whitespaces).firstIndex(of: " ") != nil)
IncognitoToggle(incognitoEnabled: $incognitoDefault)
} footer: {
VStack(alignment: .leading, spacing: 4) {
sharedProfileInfo(incognitoDefault)
Text("You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button.")
}
.frame(maxWidth: .infinity, alignment: .leading)
}
}
.alert(item: $alert) { a in planAndConnectAlert(a, dismiss: true) }
.actionSheet(item: $sheet) { s in planAndConnectActionSheet(s, dismiss: true) }
}
private func linkEditor() -> some View {
ZStack {
Group {
if connectionLink.isEmpty {
TextEditor(text: Binding.constant(NSLocalizedString("Paste the link you received to connect with your contact.", comment: "placeholder")))
.foregroundColor(.secondary)
.disabled(true)
}
TextEditor(text: $connectionLink)
.onSubmit(connect)
.textInputAutocapitalization(.never)
.disableAutocorrection(true)
.focused($linkEditorFocused)
}
.allowsTightening(false)
.padding(.horizontal, -5)
.padding(.top, -8)
.frame(height: 180, alignment: .topLeading)
.frame(maxWidth: .infinity, alignment: .leading)
}
}
private func connect() {
let link = connectionLink.trimmingCharacters(in: .whitespaces)
planAndConnect(
link,
showAlert: { alert = $0 },
showActionSheet: { sheet = $0 },
dismiss: true,
incognito: incognitoDefault
)
}
}
struct PasteToConnectView_Previews: PreviewProvider {
static var previews: some View {
PasteToConnectView()
}
}

View File

@@ -11,20 +11,12 @@ import CoreImage.CIFilterBuiltins
struct MutableQRCode: View {
@Binding var uri: String
@State private var image: UIImage?
var withLogo: Bool = true
var tintColor = UIColor(red: 0.023, green: 0.176, blue: 0.337, alpha: 1)
var body: some View {
ZStack {
if let image = image {
qrCodeImage(image)
}
}
.onAppear {
image = generateImage(uri)
}
.onChange(of: uri) { _ in
image = generateImage(uri)
}
QRCode(uri: uri, withLogo: withLogo, tintColor: tintColor)
.id("simplex-qrcode-view-for-\(uri)")
}
}
@@ -32,9 +24,10 @@ struct SimpleXLinkQRCode: View {
let uri: String
var withLogo: Bool = true
var tintColor = UIColor(red: 0.023, green: 0.176, blue: 0.337, alpha: 1)
var onShare: (() -> Void)? = nil
var body: some View {
QRCode(uri: simplexChatLink(uri), withLogo: withLogo, tintColor: tintColor)
QRCode(uri: simplexChatLink(uri), withLogo: withLogo, tintColor: tintColor, onShare: onShare)
}
}
@@ -48,8 +41,9 @@ struct QRCode: View {
let uri: String
var withLogo: Bool = true
var tintColor = UIColor(red: 0.023, green: 0.176, blue: 0.337, alpha: 1)
var onShare: (() -> Void)? = nil
@State private var image: UIImage? = nil
@State private var makeScreenshotBinding: () -> Void = {}
@State private var makeScreenshotFunc: () -> Void = {}
var body: some View {
ZStack {
@@ -70,18 +64,20 @@ struct QRCode: View {
}
}
.onAppear {
makeScreenshotBinding = {
makeScreenshotFunc = {
let size = CGSizeMake(1024 / UIScreen.main.scale, 1024 / UIScreen.main.scale)
showShareSheet(items: [makeScreenshot(geo.frame(in: .local).origin, size)])}
showShareSheet(items: [makeScreenshot(geo.frame(in: .local).origin, size)])
onShare?()
}
}
.frame(width: geo.size.width, height: geo.size.height)
}
}
.onTapGesture(perform: makeScreenshotBinding)
.onTapGesture(perform: makeScreenshotFunc)
.onAppear {
image = image ?? generateImage(uri)?.replaceColor(UIColor.black, tintColor)
image = image ?? generateImage(uri, tintColor: tintColor)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
@@ -93,13 +89,13 @@ private func qrCodeImage(_ image: UIImage) -> some View {
.textSelection(.enabled)
}
private func generateImage(_ uri: String) -> UIImage? {
private func generateImage(_ uri: String, tintColor: UIColor) -> UIImage? {
let context = CIContext()
let filter = CIFilter.qrCodeGenerator()
filter.message = Data(uri.utf8)
if let outputImage = filter.outputImage,
let cgImage = context.createCGImage(outputImage, from: outputImage.extent) {
return UIImage(cgImage: cgImage)
return UIImage(cgImage: cgImage).replaceColor(UIColor.black, tintColor)
}
return nil
}

View File

@@ -1,79 +0,0 @@
//
// ConnectContactView.swift
// SimpleX
//
// Created by Evgeny Poberezkin on 29/01/2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
import SwiftUI
import SimpleXChat
import CodeScanner
struct ScanToConnectView: View {
@Environment(\.dismiss) var dismiss: DismissAction
@AppStorage(GROUP_DEFAULT_INCOGNITO, store: groupDefaults) private var incognitoDefault = false
@State private var alert: PlanAndConnectAlert?
@State private var sheet: PlanAndConnectActionSheet?
var body: some View {
ScrollView {
VStack(alignment: .leading) {
Text("Scan QR code")
.font(.largeTitle)
.bold()
.fixedSize(horizontal: false, vertical: true)
.padding(.vertical)
CodeScannerView(codeTypes: [.qr], completion: processQRCode)
.aspectRatio(1, contentMode: .fit)
.cornerRadius(12)
IncognitoToggle(incognitoEnabled: $incognitoDefault)
.padding(.horizontal)
.padding(.vertical, 6)
.background(
RoundedRectangle(cornerRadius: 12, style: .continuous)
.fill(Color(uiColor: .systemBackground))
)
.padding(.top)
VStack(alignment: .leading, spacing: 4) {
sharedProfileInfo(incognitoDefault)
Text("If you cannot meet in person, you can **scan QR code in the video call**, or your contact can share an invitation link.")
}
.frame(maxWidth: .infinity, alignment: .leading)
.font(.footnote)
.foregroundColor(.secondary)
.padding(.horizontal)
}
.padding()
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
}
.background(Color(.systemGroupedBackground))
.alert(item: $alert) { a in planAndConnectAlert(a, dismiss: true) }
.actionSheet(item: $sheet) { s in planAndConnectActionSheet(s, dismiss: true) }
}
func processQRCode(_ resp: Result<ScanResult, ScanError>) {
switch resp {
case let .success(r):
planAndConnect(
r.string,
showAlert: { alert = $0 },
showActionSheet: { sheet = $0 },
dismiss: true,
incognito: incognitoDefault
)
case let .failure(e):
logger.error("ConnectContactView.processQRCode QR code error: \(e.localizedDescription)")
dismiss()
}
}
}
struct ConnectContactView_Previews: PreviewProvider {
static var previews: some View {
ScanToConnectView()
}
}

View File

@@ -11,12 +11,14 @@ import SimpleXChat
enum UserProfileAlert: Identifiable {
case duplicateUserError
case invalidDisplayNameError
case createUserError(error: LocalizedStringKey)
case invalidNameError(validName: String)
var id: String {
switch self {
case .duplicateUserError: return "duplicateUserError"
case .invalidDisplayNameError: return "invalidDisplayNameError"
case .createUserError: return "createUserError"
case let .invalidNameError(validName): return "invalidNameError \(validName)"
}
@@ -187,6 +189,12 @@ private func createProfile(_ displayName: String, showAlert: (UserProfileAlert)
} else {
showAlert(.duplicateUserError)
}
case .chatCmdError(_, .error(.invalidDisplayName)):
if m.currentUser == nil {
AlertManager.shared.showAlert(invalidDisplayNameAlert)
} else {
showAlert(.invalidDisplayNameError)
}
default:
let err: LocalizedStringKey = "Error: \(responseError(error))"
if m.currentUser == nil {
@@ -207,6 +215,7 @@ private func canCreateProfile(_ displayName: String) -> Bool {
func userProfileAlert(_ alert: UserProfileAlert, _ displayName: Binding<String>) -> Alert {
switch alert {
case .duplicateUserError: return duplicateUserAlert
case .invalidDisplayNameError: return invalidDisplayNameAlert
case let .createUserError(err): return creatUserErrorAlert(err)
case let .invalidNameError(name): return createInvalidNameAlert(name, displayName)
}
@@ -219,6 +228,13 @@ private var duplicateUserAlert: Alert {
)
}
private var invalidDisplayNameAlert: Alert {
Alert(
title: Text("Invalid display name!"),
message: Text("This display name is invalid. Please choose another name.")
)
}
private func creatUserErrorAlert(_ err: LocalizedStringKey) -> Alert {
Alert(
title: Text("Error creating profile!"),

View File

@@ -81,11 +81,6 @@ struct CreateSimpleXAddress: View {
DispatchQueue.main.async {
m.userAddress = UserContactLink(connReqContact: connReqContact)
}
if let u = try await apiSetProfileAddress(on: true) {
DispatchQueue.main.async {
m.updateUser(u)
}
}
await MainActor.run { progressIndicator = false }
} catch let error {
logger.error("CreateSimpleXAddress create address: \(responseError(error))")
@@ -100,7 +95,7 @@ struct CreateSimpleXAddress: View {
} label: {
Text("Create SimpleX address").font(.title)
}
Text("Your contacts in SimpleX will see it.\nYou can change it in Settings.")
Text("You can make it visible to your SimpleX contacts via Settings.")
.multilineTextAlignment(.center)
.font(.footnote)
.padding(.horizontal, 32)

View File

@@ -314,6 +314,37 @@ private let versionDescriptions: [VersionDescription] = [
),
]
),
VersionDescription(
version: "v5.5",
post: URL(string: "https://simplex.chat/blog/20240124-simplex-chat-infrastructure-costs-v5-5-simplex-ux-private-notes-group-history.html"),
features: [
FeatureDescription(
icon: "folder",
title: "Private notes",
description: "With encrypted files and media."
),
FeatureDescription(
icon: "link",
title: "Paste link to connect!",
description: "Search bar accepts invitation links."
),
FeatureDescription(
icon: "bubble.left.and.bubble.right",
title: "Join group conversations",
description: "Recent history and improved [directory bot](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion)."
),
FeatureDescription(
icon: "battery.50",
title: "Improved message delivery",
description: "With reduced battery usage."
),
FeatureDescription(
icon: "character",
title: "Turkish interface",
description: "Thanks to the users [contribute via Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!"
),
]
)
]
private let lastVersion = versionDescriptions.last!.version

View File

@@ -332,7 +332,7 @@ struct ConnectDesktopView: View {
private func scanDesctopAddressView() -> some View {
Section("Scan QR code from desktop") {
CodeScannerView(codeTypes: [.qr], completion: processDesktopQRCode)
CodeScannerView(codeTypes: [.qr], scanMode: .oncePerCode, completion: processDesktopQRCode)
.aspectRatio(1, contentMode: .fit)
.cornerRadius(12)
.listRowBackground(Color.clear)

View File

@@ -51,9 +51,9 @@ struct AdvancedNetworkSettings: View {
}
.disabled(currentNetCfg == NetCfg.proxyDefaults)
timeoutSettingPicker("TCP connection timeout", selection: $netCfg.tcpConnectTimeout, values: [5_000000, 7_500000, 10_000000, 15_000000, 20_000000, 30_000000, 45_000000], label: secondsLabel)
timeoutSettingPicker("Protocol timeout", selection: $netCfg.tcpTimeout, values: [3_000000, 5_000000, 7_000000, 10_000000, 15_000000, 20_000000, 30_000000], label: secondsLabel)
timeoutSettingPicker("Protocol timeout per KB", selection: $netCfg.tcpTimeoutPerKb, values: [15_000, 30_000, 60_000, 90_000, 120_000], label: secondsLabel)
timeoutSettingPicker("TCP connection timeout", selection: $netCfg.tcpConnectTimeout, values: [7_500000, 10_000000, 15_000000, 20_000000, 30_000000, 45_000000], label: secondsLabel)
timeoutSettingPicker("Protocol timeout", selection: $netCfg.tcpTimeout, values: [5_000000, 7_000000, 10_000000, 15_000000, 20_000000, 30_000000], label: secondsLabel)
timeoutSettingPicker("Protocol timeout per KB", selection: $netCfg.tcpTimeoutPerKb, values: [15_000, 30_000, 45_000, 60_000, 90_000, 120_000], label: secondsLabel)
timeoutSettingPicker("PING interval", selection: $netCfg.smpPingInterval, values: [120_000000, 300_000000, 600_000000, 1200_000000, 2400_000000, 3600_000000], label: secondsLabel)
intSettingPicker("PING count", selection: $netCfg.smpPingCount, values: [1, 2, 3, 5, 8], label: "")
Toggle("Enable TCP keep-alive", isOn: $enableKeepAlive)

View File

@@ -10,24 +10,23 @@ import SwiftUI
struct IncognitoHelp: View {
var body: some View {
VStack(alignment: .leading) {
List {
Text("Incognito mode")
.font(.largeTitle)
.bold()
.fixedSize(horizontal: false, vertical: true)
.padding(.vertical)
ScrollView {
VStack(alignment: .leading) {
Group {
Text("Incognito mode protects your privacy by using a new random profile for each contact.")
Text("It allows having many anonymous connections without any shared data between them in a single chat profile.")
Text("When you share an incognito profile with somebody, this profile will be used for the groups they invite you to.")
}
.padding(.bottom)
}
.listRowBackground(Color.clear)
.listRowSeparator(.hidden)
.listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
VStack(alignment: .leading, spacing: 18) {
Text("Incognito mode protects your privacy by using a new random profile for each contact.")
Text("It allows having many anonymous connections without any shared data between them in a single chat profile.")
Text("When you share an incognito profile with somebody, this profile will be used for the groups they invite you to.")
Text("Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode).")
}
.listRowBackground(Color.clear)
}
.frame(maxWidth: .infinity)
.padding()
}
}

View File

@@ -14,9 +14,6 @@ struct NotificationsView: View {
@State private var notificationMode: NotificationsMode = ChatModel.shared.notificationMode
@State private var showAlert: NotificationAlert?
@State private var legacyDatabase = dbContainerGroupDefault.get() == .documents
// @AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false
// @AppStorage(GROUP_DEFAULT_NTF_ENABLE_LOCAL, store: groupDefaults) private var ntfEnableLocal = false
// @AppStorage(GROUP_DEFAULT_NTF_ENABLE_PERIODIC, store: groupDefaults) private var ntfEnablePeriodic = false
var body: some View {
List {
@@ -88,13 +85,6 @@ struct NotificationsView: View {
.padding(.top, 1)
}
}
// if developerTools {
// Section(String("Experimental")) {
// Toggle(String("Always enable local"), isOn: $ntfEnableLocal)
// Toggle(String("Always enable periodic"), isOn: $ntfEnablePeriodic)
// }
// }
}
.disabled(legacyDatabase)
}
@@ -119,7 +109,7 @@ struct NotificationsView: View {
private func ntfModeAlertTitle(_ mode: NotificationsMode) -> LocalizedStringKey {
switch mode {
case .off: return "Turn off notifications?"
case .off: return "Use only local notifications?"
case .periodic: return "Enable periodic notifications?"
case .instant: return "Enable instant notifications?"
}

View File

@@ -63,7 +63,6 @@ struct PreferencesView: View {
private func featureFooter(_ feature: ChatFeature, _ allowFeature: Binding<FeatureAllowed>) -> some View {
Text(feature.allowDescription(allowFeature.wrappedValue))
.frame(height: 36, alignment: .topLeading)
}
private func savePreferences() {

View File

@@ -467,6 +467,7 @@ struct SimplexLockView: View {
switch a {
case .enableAuth:
SetAppPasscodeView {
m.contentViewAccessAuthenticated = true
laLockDelay = 30
prefPerformLA = true
showChangePassword = true
@@ -490,14 +491,23 @@ struct SimplexLockView: View {
showLAAlert(.laPasscodeNotChangedAlert)
}
case .enableSelfDestruct:
SetAppPasscodeView(passcodeKeychain: kcSelfDestructPassword, title: "Set passcode", reason: NSLocalizedString("Enable self-destruct passcode", comment: "set passcode view")) {
SetAppPasscodeView(
passcodeKeychain: kcSelfDestructPassword,
prohibitedPasscodeKeychain: kcAppPassword,
title: "Set passcode",
reason: NSLocalizedString("Enable self-destruct passcode", comment: "set passcode view")
) {
updateSelfDestruct()
showLAAlert(.laSelfDestructPasscodeSetAlert)
} cancel: {
revertSelfDestruct()
}
case .changeSelfDestructPasscode:
SetAppPasscodeView(passcodeKeychain: kcSelfDestructPassword, reason: NSLocalizedString("Change self-destruct passcode", comment: "set passcode view")) {
SetAppPasscodeView(
passcodeKeychain: kcSelfDestructPassword,
prohibitedPasscodeKeychain: kcAppPassword,
reason: NSLocalizedString("Change self-destruct passcode", comment: "set passcode view")
) {
showLAAlert(.laSelfDestructPasscodeChangedAlert)
} cancel: {
showLAAlert(.laPasscodeNotChangedAlert)
@@ -619,6 +629,7 @@ struct SimplexLockView: View {
authenticate(reason: NSLocalizedString("Enable SimpleX Lock", comment: "authentication reason")) { laResult in
switch laResult {
case .success:
m.contentViewAccessAuthenticated = true
prefPerformLA = true
laAlert = .laTurnedOnAlert
case .failed:

View File

@@ -21,7 +21,7 @@ struct ScanProtocolServer: View {
.font(.largeTitle)
.bold()
.padding(.vertical)
CodeScannerView(codeTypes: [.qr], completion: processQRCode)
CodeScannerView(codeTypes: [.qr], scanMode: .oncePerCode, completion: processQRCode)
.aspectRatio(1, contentMode: .fit)
.cornerRadius(12)
.padding(.top)

View File

@@ -95,6 +95,12 @@ let appDefaults: [String: Any] = [
DEFAULT_CONNECT_REMOTE_VIA_MULTICAST_AUTO: true,
]
// not used anymore
enum ConnectViaLinkTab: String {
case scan
case paste
}
enum SimpleXLinkMode: String, Identifiable {
case description
case full
@@ -153,37 +159,42 @@ struct SettingsView: View {
}
@ViewBuilder func settingsView() -> some View {
let user: User = chatModel.currentUser!
let user = chatModel.currentUser
NavigationView {
List {
Section("You") {
NavigationLink {
UserProfile()
.navigationTitle("Your current profile")
} label: {
ProfilePreview(profileOf: user)
.padding(.leading, -8)
if let user = user {
NavigationLink {
UserProfile()
.navigationTitle("Your current profile")
} label: {
ProfilePreview(profileOf: user)
.padding(.leading, -8)
}
}
NavigationLink {
UserProfilesView()
UserProfilesView(showSettings: $showSettings)
} label: {
settingsRow("person.crop.rectangle.stack") { Text("Your chat profiles") }
}
NavigationLink {
UserAddressView(shareViaProfile: chatModel.currentUser!.addressShared)
.navigationTitle("SimpleX address")
.navigationBarTitleDisplayMode(.large)
} label: {
settingsRow("qrcode") { Text("Your SimpleX address") }
}
NavigationLink {
PreferencesView(profile: user.profile, preferences: user.fullPreferences, currentPreferences: user.fullPreferences)
.navigationTitle("Your preferences")
} label: {
settingsRow("switch.2") { Text("Chat preferences") }
if let user = user {
NavigationLink {
UserAddressView(shareViaProfile: user.addressShared)
.navigationTitle("SimpleX address")
.navigationBarTitleDisplayMode(.large)
} label: {
settingsRow("qrcode") { Text("Your SimpleX address") }
}
NavigationLink {
PreferencesView(profile: user.profile, preferences: user.fullPreferences, currentPreferences: user.fullPreferences)
.navigationTitle("Your preferences")
} label: {
settingsRow("switch.2") { Text("Chat preferences") }
}
}
NavigationLink {
@@ -244,12 +255,14 @@ struct SettingsView: View {
}
Section("Help") {
NavigationLink {
ChatHelp(showSettings: $showSettings)
.navigationTitle("Welcome \(user.displayName)!")
.frame(maxHeight: .infinity, alignment: .top)
} label: {
settingsRow("questionmark") { Text("How to use it") }
if let user = user {
NavigationLink {
ChatHelp(showSettings: $showSettings)
.navigationTitle("Welcome \(user.displayName)!")
.frame(maxHeight: .infinity, alignment: .top)
} label: {
settingsRow("questionmark") { Text("How to use it") }
}
}
NavigationLink {
WhatsNewView(viaSettings: true)

View File

@@ -190,7 +190,8 @@ struct UserAddressView: View {
@ViewBuilder private func existingAddressView(_ userAddress: UserContactLink) -> some View {
Section {
MutableQRCode(uri: Binding.constant(simplexChatLink(userAddress.connReqContact)))
SimpleXLinkQRCode(uri: userAddress.connReqContact)
.id("simplex-contact-address-qrcode-\(userAddress.connReqContact)")
shareQRCodeButton(userAddress)
if MFMailComposeViewController.canSendMail() {
shareViaEmailButton(userAddress)

View File

@@ -120,8 +120,10 @@ struct UserProfile: View {
}
}
.sheet(isPresented: $showImagePicker) {
LibraryImagePicker(image: $chosenImage) {
didSelectItem in showImagePicker = false
LibraryImagePicker(image: $chosenImage) { _ in
await MainActor.run {
showImagePicker = false
}
}
}
.onChange(of: chosenImage) { image in

View File

@@ -8,6 +8,7 @@ import SimpleXChat
struct UserProfilesView: View {
@EnvironmentObject private var m: ChatModel
@Binding var showSettings: Bool
@Environment(\.editMode) private var editMode
@AppStorage(DEFAULT_SHOW_HIDDEN_PROFILES_NOTICE) private var showHiddenProfilesNotice = true
@AppStorage(DEFAULT_SHOW_MUTE_PROFILE_ALERT) private var showMuteProfileAlert = true
@@ -25,7 +26,6 @@ struct UserProfilesView: View {
private enum UserProfilesAlert: Identifiable {
case deleteUser(user: User, delSMPQueues: Bool)
case cantDeleteLastUser
case hiddenProfilesNotice
case muteProfileAlert
case activateUserError(error: String)
@@ -34,7 +34,6 @@ struct UserProfilesView: View {
var id: String {
switch self {
case let .deleteUser(user, delSMPQueues): return "deleteUser \(user.userId) \(delSMPQueues)"
case .cantDeleteLastUser: return "cantDeleteLastUser"
case .hiddenProfilesNotice: return "hiddenProfilesNotice"
case .muteProfileAlert: return "muteProfileAlert"
case let .activateUserError(err): return "activateUserError \(err)"
@@ -78,7 +77,7 @@ struct UserProfilesView: View {
Section {
let users = filteredUsers()
let v = ForEach(users) { u in
userView(u.user, allowDelete: users.count > 1)
userView(u.user)
}
if #available(iOS 16, *) {
v.onDelete { indexSet in
@@ -146,13 +145,6 @@ struct UserProfilesView: View {
},
secondaryButton: .cancel()
)
case .cantDeleteLastUser:
return Alert(
title: Text("Can't delete user profile!"),
message: m.users.count > 1
? Text("There should be at least one visible user profile.")
: Text("There should be at least one user profile.")
)
case .hiddenProfilesNotice:
return Alert(
title: Text("Make profile private!"),
@@ -280,11 +272,21 @@ struct UserProfilesView: View {
if let newActive = m.users.first(where: { u in !u.user.activeUser && !u.user.hidden }) {
try await changeActiveUserAsync_(newActive.user.userId, viewPwd: nil)
try await deleteUser()
} else {
// Deleting the last visible user while having hidden one(s)
try await deleteUser()
try await changeActiveUserAsync_(nil, viewPwd: nil)
await MainActor.run {
onboardingStageDefault.set(.step1_SimpleXInfo)
m.onboardingStage = .step1_SimpleXInfo
showSettings = false
}
}
} else {
try await deleteUser()
}
} catch let error {
logger.error("Error deleting user profile: \(error)")
let a = getErrorAlert(error, "Error deleting user profile")
alert = .error(title: a.title, error: a.message)
}
@@ -295,7 +297,7 @@ struct UserProfilesView: View {
}
}
@ViewBuilder private func userView(_ user: User, allowDelete: Bool) -> some View {
@ViewBuilder private func userView(_ user: User) -> some View {
let v = Button {
Task {
do {
@@ -323,9 +325,7 @@ struct UserProfilesView: View {
}
}
}
.disabled(user.activeUser)
.foregroundColor(.primary)
.deleteDisabled(!allowDelete)
.swipeActions(edge: .leading, allowsFullSwipe: true) {
if user.hidden {
Button("Unhide") {
@@ -361,8 +361,6 @@ struct UserProfilesView: View {
}
if #available(iOS 16, *) {
v
} else if !allowDelete {
v
} else {
v.swipeActions(edge: .trailing, allowsFullSwipe: true) {
Button("Delete", role: .destructive) {
@@ -373,12 +371,8 @@ struct UserProfilesView: View {
}
private func confirmDeleteUser(_ user: User) {
if m.users.count > 1 && (user.hidden || visibleUsersCount > 1) {
showDeleteConfirmation = true
userToDelete = user
} else {
alert = .cantDeleteLastUser
}
showDeleteConfirmation = true
userToDelete = user
}
private func setUserPrivacy(_ user: User, successAlert: UserProfilesAlert? = nil, _ api: @escaping () async throws -> User) {
@@ -409,6 +403,6 @@ public func chatPasswordHash(_ pwd: String, _ salt: String) -> String {
struct UserProfilesView_Previews: PreviewProvider {
static var previews: some View {
UserProfilesView()
UserProfilesView(showSettings: Binding.constant(true))
}
}

View File

@@ -89,6 +89,7 @@
</trans-unit>
<trans-unit id="%@ and %@" xml:space="preserve">
<source>%@ and %@</source>
<target>%@ a %@</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%@ and %@ connected" xml:space="preserve">
@@ -103,6 +104,7 @@
</trans-unit>
<trans-unit id="%@ connected" xml:space="preserve">
<source>%@ connected</source>
<target>%@ připojen</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%@ is connected!" xml:space="preserve">
@@ -212,6 +214,10 @@
<source>%lld messages blocked</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%lld messages blocked by admin" xml:space="preserve">
<source>%lld messages blocked by admin</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%lld messages marked deleted" xml:space="preserve">
<source>%lld messages marked deleted</source>
<note>No comment provided by engineer.</note>
@@ -303,14 +309,17 @@
<target>)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="**Add contact**: to create a new invitation link, or connect via a link you received." xml:space="preserve">
<source>**Add contact**: to create a new invitation link, or connect via a link you received.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="**Add new contact**: to create your one-time QR Code for your contact." xml:space="preserve">
<source>**Add new contact**: to create your one-time QR Code or link for your contact.</source>
<target>**Přidat nový kontakt**: pro vytvoření jednorázového QR kódu nebo odkazu pro váš kontakt.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="**Create link / QR code** for your contact to use." xml:space="preserve">
<source>**Create link / QR code** for your contact to use.</source>
<target>**Vytvořte odkaz / QR kód** pro váš kontakt.</target>
<trans-unit id="**Create group**: to create a new group." xml:space="preserve">
<source>**Create group**: to create a new group.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have." xml:space="preserve">
@@ -323,11 +332,6 @@
<target>**Nejsoukromější**: nepoužívejte server oznámení SimpleX Chat, pravidelně kontrolujte zprávy na pozadí (závisí na tom, jak často aplikaci používáte).</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="**Paste received link** or open it in the browser and tap **Open in mobile app**." xml:space="preserve">
<source>**Paste received link** or open it in the browser and tap **Open in mobile app**.</source>
<target>**Vložte přijatý odkaz** nebo jej otevřete v prohlížeči a klepněte na **Otevřít v mobilní aplikaci**.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="**Please note**: you will NOT be able to recover or change passphrase if you lose it." xml:space="preserve">
<source>**Please note**: you will NOT be able to recover or change passphrase if you lose it.</source>
<target>**Upozornění**: Pokud heslo ztratíte, NEBUDETE jej moci obnovit ani změnit.</target>
@@ -338,11 +342,6 @@
<target>**Doporučeno**: Token zařízení a oznámení se odesílají na oznamovací server SimpleX Chat, ale nikoli obsah, velikost nebo od koho jsou zprávy.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="**Scan QR code**: to connect to your contact in person or via video call." xml:space="preserve">
<source>**Scan QR code**: to connect to your contact in person or via video call.</source>
<target>** Naskenujte QR kód**: pro připojení ke kontaktu osobně nebo prostřednictvím videohovoru.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="**Warning**: Instant push notifications require passphrase saved in Keychain." xml:space="preserve">
<source>**Warning**: Instant push notifications require passphrase saved in Keychain.</source>
<target>**Upozornění**: Okamžitě doručovaná oznámení vyžadují přístupové heslo uložené v Klíčence.</target>
@@ -372,7 +371,7 @@
<source>- connect to [directory service](simplex:/contact#/?v=1-4&amp;smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)!
- delivery receipts (up to 20 members).
- faster and more stable.</source>
<target>- připojit k [adresářová služba](simplex:/contact#/?v=1-4&amp;smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.cibule) (BETA)!
<target>- připojit k [adresářová služba](simplex:/contact#/?v=1-4&amp;smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)!
- doručenky (až 20 členů).
- Rychlejší a stabilnější.</target>
<note>No comment provided by engineer.</note>
@@ -440,11 +439,6 @@
<target>1 týden</target>
<note>time interval</note>
</trans-unit>
<trans-unit id="1-time link" xml:space="preserve">
<source>1-time link</source>
<target>Jednorázový odkaz</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="5 minutes" xml:space="preserve">
<source>5 minutes</source>
<target>5 minut</target>
@@ -560,6 +554,10 @@
<target>Přidejte adresu do svého profilu, aby ji vaše kontakty mohly sdílet s dalšími lidmi. Aktualizace profilu bude zaslána vašim kontaktům.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Add contact" xml:space="preserve">
<source>Add contact</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Add preset servers" xml:space="preserve">
<source>Add preset servers</source>
<target>Přidejte přednastavené servery</target>
@@ -630,6 +628,10 @@
<target>Všichni členové skupiny zůstanou připojeni.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="All messages will be deleted - this cannot be undone!" xml:space="preserve">
<source>All messages will be deleted - this cannot be undone!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you." xml:space="preserve">
<source>All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you.</source>
<target>Všechny zprávy budou smazány tuto akci nelze vrátit zpět! Zprávy budou smazány POUZE pro vás.</target>
@@ -664,9 +666,9 @@
<target>Povolte mizící zprávy, pouze pokud vám to váš kontakt dovolí.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Allow irreversible message deletion only if your contact allows it to you." xml:space="preserve">
<source>Allow irreversible message deletion only if your contact allows it to you.</source>
<target>Povolte nevratné smazání zprávy pouze v případě, že vám to váš kontakt dovolí.</target>
<trans-unit id="Allow irreversible message deletion only if your contact allows it to you. (24 hours)" xml:space="preserve">
<source>Allow irreversible message deletion only if your contact allows it to you. (24 hours)</source>
<target>Povolte nevratné smazání zprávy pouze v případě, že vám to váš kontakt dovolí. (24 hodin)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Allow message reactions only if your contact allows them." xml:space="preserve">
@@ -689,9 +691,9 @@
<target>Povolit odesílání mizících zpráv.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Allow to irreversibly delete sent messages." xml:space="preserve">
<source>Allow to irreversibly delete sent messages.</source>
<target>Povolit nevratné smazání odeslaných zpráv.</target>
<trans-unit id="Allow to irreversibly delete sent messages. (24 hours)" xml:space="preserve">
<source>Allow to irreversibly delete sent messages. (24 hours)</source>
<target>Povolit nevratné smazání odeslaných zpráv. (24 hodin)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Allow to send files and media." xml:space="preserve">
@@ -724,9 +726,9 @@
<target>Povolte svým kontaktům vám volat.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Allow your contacts to irreversibly delete sent messages." xml:space="preserve">
<source>Allow your contacts to irreversibly delete sent messages.</source>
<target>Umožněte svým kontaktům nevratně odstranit odeslané zprávy.</target>
<trans-unit id="Allow your contacts to irreversibly delete sent messages. (24 hours)" xml:space="preserve">
<source>Allow your contacts to irreversibly delete sent messages. (24 hours)</source>
<target>Umožněte svým kontaktům nevratně odstranit odeslané zprávy. (24 hodin)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Allow your contacts to send disappearing messages." xml:space="preserve">
@@ -899,6 +901,10 @@
<source>Block</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Block for all" xml:space="preserve">
<source>Block for all</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Block group members" xml:space="preserve">
<source>Block group members</source>
<note>No comment provided by engineer.</note>
@@ -907,18 +913,26 @@
<source>Block member</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Block member for all?" xml:space="preserve">
<source>Block member for all?</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Block member?" xml:space="preserve">
<source>Block member?</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Blocked by admin" xml:space="preserve">
<source>Blocked by admin</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Both you and your contact can add message reactions." xml:space="preserve">
<source>Both you and your contact can add message reactions.</source>
<target>Vy i váš kontakt můžete přidávat reakce na zprávy.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Both you and your contact can irreversibly delete sent messages." xml:space="preserve">
<source>Both you and your contact can irreversibly delete sent messages.</source>
<target>Vy i váš kontakt můžete nevratně mazat odeslané zprávy.</target>
<trans-unit id="Both you and your contact can irreversibly delete sent messages. (24 hours)" xml:space="preserve">
<source>Both you and your contact can irreversibly delete sent messages. (24 hours)</source>
<target>Vy i váš kontakt můžete nevratně mazat odeslané zprávy. (24 hodin)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Both you and your contact can make calls." xml:space="preserve">
@@ -956,9 +970,8 @@
<target>Hovory</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Can't delete user profile!" xml:space="preserve">
<source>Can't delete user profile!</source>
<target>Nemohu smazat uživatelský profil!</target>
<trans-unit id="Camera not available" xml:space="preserve">
<source>Camera not available</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Can't invite contact!" xml:space="preserve">
@@ -1072,6 +1085,10 @@
<target>Chat je zastaven</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Chat is stopped. If you already used this database on another device, you should transfer it back before starting chat." xml:space="preserve">
<source>Chat is stopped. If you already used this database on another device, you should transfer it back before starting chat.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Chat preferences" xml:space="preserve">
<source>Chat preferences</source>
<target>Předvolby chatu</target>
@@ -1117,6 +1134,10 @@
<target>Vyčistit konverzaci?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Clear private notes?" xml:space="preserve">
<source>Clear private notes?</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Clear verification" xml:space="preserve">
<source>Clear verification</source>
<target>Zrušte ověření</target>
@@ -1208,11 +1229,6 @@ This is your own one-time link!</source>
<target>Připojte se prostřednictvím odkazu</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connect via link / QR code" xml:space="preserve">
<source>Connect via link / QR code</source>
<target>Připojit se prostřednictvím odkazu / QR kódu</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connect via one-time link" xml:space="preserve">
<source>Connect via one-time link</source>
<target>Připojit se jednorázovým odkazem</target>
@@ -1380,11 +1396,6 @@ This is your own one-time link!</source>
<target>Vytvořit nový profil v [desktop app](https://simplex.chat/downloads/). 💻</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Create one-time invitation link" xml:space="preserve">
<source>Create one-time invitation link</source>
<target>Vytvořit jednorázovou pozvánku</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Create profile" xml:space="preserve">
<source>Create profile</source>
<note>No comment provided by engineer.</note>
@@ -1404,11 +1415,23 @@ This is your own one-time link!</source>
<target>Vytvořte si profil</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Created at" xml:space="preserve">
<source>Created at</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Created at: %@" xml:space="preserve">
<source>Created at: %@</source>
<note>copied message info</note>
</trans-unit>
<trans-unit id="Created on %@" xml:space="preserve">
<source>Created on %@</source>
<target>Vytvořeno na %@</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Creating link…" xml:space="preserve">
<source>Creating link…</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Current Passcode" xml:space="preserve">
<source>Current Passcode</source>
<target>Aktuální heslo</target>
@@ -1880,6 +1903,10 @@ This cannot be undone!</source>
<target>Udělat později</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Do not send history to new members." xml:space="preserve">
<source>Do not send history to new members.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Don't create address" xml:space="preserve">
<source>Don't create address</source>
<target>Nevytvářet adresu</target>
@@ -1950,6 +1977,10 @@ This cannot be undone!</source>
<target>Povolit automatické mazání zpráv?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Enable camera access" xml:space="preserve">
<source>Enable camera access</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Enable for all" xml:space="preserve">
<source>Enable for all</source>
<target>Povolit pro všechny</target>
@@ -2015,6 +2046,10 @@ This cannot be undone!</source>
<target>Šifrovaná zpráva nebo jiná událost</target>
<note>notification</note>
</trans-unit>
<trans-unit id="Encrypted message: app is stopped" xml:space="preserve">
<source>Encrypted message: app is stopped</source>
<note>notification</note>
</trans-unit>
<trans-unit id="Encrypted message: database error" xml:space="preserve">
<source>Encrypted message: database error</source>
<target>Šifrovaná zpráva: chyba databáze</target>
@@ -2155,6 +2190,10 @@ This cannot be undone!</source>
<target>Chyba vytvoření kontaktu člena</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error creating message" xml:space="preserve">
<source>Error creating message</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error creating profile!" xml:space="preserve">
<source>Error creating profile!</source>
<target>Chyba při vytváření profilu!</target>
@@ -2240,6 +2279,10 @@ This cannot be undone!</source>
<target>Chyba načítání %@ serverů</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error opening chat" xml:space="preserve">
<source>Error opening chat</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error receiving file" xml:space="preserve">
<source>Error receiving file</source>
<target>Chyba při příjmu souboru</target>
@@ -2280,6 +2323,10 @@ This cannot be undone!</source>
<target>Chyba ukládání hesla uživatele</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error scanning code: %@" xml:space="preserve">
<source>Error scanning code: %@</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error sending email" xml:space="preserve">
<source>Error sending email</source>
<target>Chyba odesílání e-mailu</target>
@@ -2604,9 +2651,9 @@ This cannot be undone!</source>
<target>Členové skupin mohou přidávat reakce na zprávy.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Group members can irreversibly delete sent messages." xml:space="preserve">
<source>Group members can irreversibly delete sent messages.</source>
<target>Členové skupiny mohou nevratně mazat odeslané zprávy.</target>
<trans-unit id="Group members can irreversibly delete sent messages. (24 hours)" xml:space="preserve">
<source>Group members can irreversibly delete sent messages. (24 hours)</source>
<target>Členové skupiny mohou nevratně mazat odeslané zprávy. (24 hodin)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Group members can send direct messages." xml:space="preserve">
@@ -2714,6 +2761,10 @@ This cannot be undone!</source>
<target>Historie</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="History is not sent to new members." xml:space="preserve">
<source>History is not sent to new members.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="How SimpleX works" xml:space="preserve">
<source>How SimpleX works</source>
<target>Jak SimpleX funguje</target>
@@ -2749,11 +2800,6 @@ This cannot be undone!</source>
<target>Pokud se nemůžete setkat osobně, zobrazte QR kód ve videohovoru nebo sdílejte odkaz.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="If you cannot meet in person, you can **scan QR code in the video call**, or your contact can share an invitation link." xml:space="preserve">
<source>If you cannot meet in person, you can **scan QR code in the video call**, or your contact can share an invitation link.</source>
<target>Pokud se nemůžete setkat osobně, můžete **naskenovat QR kód během videohovoru**, nebo váš kontakt může sdílet odkaz na pozvánku.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="If you enter this passcode when opening the app, all app data will be irreversibly removed!" xml:space="preserve">
<source>If you enter this passcode when opening the app, all app data will be irreversibly removed!</source>
<target>Pokud tento přístupový kód zadáte při otevření aplikace, všechna data budou nenávratně smazána!</target>
@@ -2809,6 +2855,10 @@ This cannot be undone!</source>
<target>Import databáze</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Improved message delivery" xml:space="preserve">
<source>Improved message delivery</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Improved privacy and security" xml:space="preserve">
<source>Improved privacy and security</source>
<target>Vylepšená ochrana soukromí a zabezpečení</target>
@@ -2909,15 +2959,31 @@ This cannot be undone!</source>
<target>Rozhranní</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Invalid QR code" xml:space="preserve">
<source>Invalid QR code</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Invalid connection link" xml:space="preserve">
<source>Invalid connection link</source>
<target>Neplatný odkaz na spojení</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Invalid display name!" xml:space="preserve">
<source>Invalid display name!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Invalid link" xml:space="preserve">
<source>Invalid link</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Invalid name!" xml:space="preserve">
<source>Invalid name!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Invalid response" xml:space="preserve">
<source>Invalid response</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Invalid server address!" xml:space="preserve">
<source>Invalid server address!</source>
<target>Neplatná adresa serveru!</target>
@@ -3009,6 +3075,10 @@ This cannot be undone!</source>
<target>Připojit ke skupině</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Join group conversations" xml:space="preserve">
<source>Join group conversations</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Join group?" xml:space="preserve">
<source>Join group?</source>
<note>No comment provided by engineer.</note>
@@ -3032,10 +3102,18 @@ This is your link for group %@!</source>
<target>Připojování ke skupině</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Keep" xml:space="preserve">
<source>Keep</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Keep the app open to use it from desktop" xml:space="preserve">
<source>Keep the app open to use it from desktop</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Keep unused invitation?" xml:space="preserve">
<source>Keep unused invitation?</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Keep your connections" xml:space="preserve">
<source>Keep your connections</source>
<target>Zachovat vaše připojení</target>
@@ -3118,6 +3196,11 @@ This is your link for group %@!</source>
<target>Živé zprávy</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Local" xml:space="preserve">
<source>Local</source>
<target>Místní</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Local name" xml:space="preserve">
<source>Local name</source>
<target>Místní název</target>
@@ -3357,6 +3440,10 @@ This is your link for group %@!</source>
<target>Nové heslo</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="New chat" xml:space="preserve">
<source>New chat</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="New contact request" xml:space="preserve">
<source>New contact request</source>
<target>Žádost o nový kontakt</target>
@@ -3480,16 +3567,15 @@ This is your link for group %@!</source>
- zakázat členy (role "pozorovatel")</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="OK" xml:space="preserve">
<source>OK</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Off" xml:space="preserve">
<source>Off</source>
<target>Vypnout</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Off (Local)" xml:space="preserve">
<source>Off (Local)</source>
<target>Vypnuto (místní)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Ok" xml:space="preserve">
<source>Ok</source>
<target>Ok</target>
@@ -3550,9 +3636,9 @@ This is your link for group %@!</source>
<target>Reakce na zprávy můžete přidávat pouze vy.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Only you can irreversibly delete messages (your contact can mark them for deletion)." xml:space="preserve">
<source>Only you can irreversibly delete messages (your contact can mark them for deletion).</source>
<target>Nevratně mazat zprávy můžete pouze vy (váš kontakt je může označit ke smazání).</target>
<trans-unit id="Only you can irreversibly delete messages (your contact can mark them for deletion). (24 hours)" xml:space="preserve">
<source>Only you can irreversibly delete messages (your contact can mark them for deletion). (24 hours)</source>
<target>Nevratně mazat zprávy můžete pouze vy (váš kontakt je může označit ke smazání). (24 hodin)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Only you can make calls." xml:space="preserve">
@@ -3575,9 +3661,9 @@ This is your link for group %@!</source>
<target>Reakce na zprávy může přidávat pouze váš kontakt.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Only your contact can irreversibly delete messages (you can mark them for deletion)." xml:space="preserve">
<source>Only your contact can irreversibly delete messages (you can mark them for deletion).</source>
<target>Nevratně mazat zprávy může pouze váš kontakt (vy je můžete označit ke smazání).</target>
<trans-unit id="Only your contact can irreversibly delete messages (you can mark them for deletion). (24 hours)" xml:space="preserve">
<source>Only your contact can irreversibly delete messages (you can mark them for deletion). (24 hours)</source>
<target>Nevratně mazat zprávy může pouze váš kontakt (vy je můžete označit ke smazání). (24 hodin)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Only your contact can make calls." xml:space="preserve">
@@ -3629,9 +3715,16 @@ This is your link for group %@!</source>
<target>Protokol a kód s otevřeným zdrojovým kódem - servery může provozovat kdokoli.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Opening database…" xml:space="preserve">
<source>Opening database…</source>
<target>Otvírání databáze…</target>
<trans-unit id="Opening app…" xml:space="preserve">
<source>Opening app…</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Or scan QR code" xml:space="preserve">
<source>Or scan QR code</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Or show this code" xml:space="preserve">
<source>Or show this code</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="PING count" xml:space="preserve">
@@ -3674,10 +3767,9 @@ This is your link for group %@!</source>
<target>Heslo k zobrazení</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Paste" xml:space="preserve">
<source>Paste</source>
<target>Vložit</target>
<note>No comment provided by engineer.</note>
<trans-unit id="Past member %@" xml:space="preserve">
<source>Past member %@</source>
<note>past/unknown group member</note>
</trans-unit>
<trans-unit id="Paste desktop address" xml:space="preserve">
<source>Paste desktop address</source>
@@ -3688,15 +3780,13 @@ This is your link for group %@!</source>
<target>Vložit obrázek</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Paste received link" xml:space="preserve">
<source>Paste received link</source>
<target>Vložení přijatého odkazu</target>
<trans-unit id="Paste link to connect!" xml:space="preserve">
<source>Paste link to connect!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Paste the link you received to connect with your contact." xml:space="preserve">
<source>Paste the link you received to connect with your contact.</source>
<target>Vložte odkaz, který jste obdrželi, do pole níže a spojte se se svým kontaktem.</target>
<note>placeholder</note>
<trans-unit id="Paste the link you received" xml:space="preserve">
<source>Paste the link you received</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="People can connect to you only via the links you share." xml:space="preserve">
<source>People can connect to you only via the links you share.</source>
@@ -3733,6 +3823,11 @@ This is your link for group %@!</source>
<target>Zkontrolujte prosím nastavení své i svého kontaktu.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Please contact developers.&#10;Error: %@" xml:space="preserve">
<source>Please contact developers.
Error: %@</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Please contact group admin." xml:space="preserve">
<source>Please contact group admin.</source>
<target>Kontaktujte prosím správce skupiny.</target>
@@ -3818,6 +3913,10 @@ This is your link for group %@!</source>
<target>Soukromé názvy souborů</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Private notes" xml:space="preserve">
<source>Private notes</source>
<note>name of notes to self</note>
</trans-unit>
<trans-unit id="Profile and server connections" xml:space="preserve">
<source>Profile and server connections</source>
<target>Profil a připojení k serveru</target>
@@ -3936,6 +4035,10 @@ This is your link for group %@!</source>
<target>Další informace naleznete v [Uživatelské příručce](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address).</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)." xml:space="preserve">
<source>Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode).</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends)." xml:space="preserve">
<source>Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends).</source>
<target>Přečtěte si více v [Uživatelské příručce](https://simplex.chat/docs/guide/readme.html#connect-to-friends).</target>
@@ -3991,6 +4094,10 @@ This is your link for group %@!</source>
<target>Příjem přes</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Recent history and improved [directory bot](simplex:/contact#/?v=1-4&amp;smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion)." xml:space="preserve">
<source>Recent history and improved [directory bot](simplex:/contact#/?v=1-4&amp;smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion).</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Recipients see updates as you type them." xml:space="preserve">
<source>Recipients see updates as you type them.</source>
<target>Příjemci uvidí aktualizace během jejich psaní.</target>
@@ -4144,6 +4251,10 @@ This is your link for group %@!</source>
<target>Chyba obnovení databáze</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Retry" xml:space="preserve">
<source>Retry</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Reveal" xml:space="preserve">
<source>Reveal</source>
<target>Odhalit</target>
@@ -4269,6 +4380,10 @@ This is your link for group %@!</source>
<target>Uložené servery WebRTC ICE budou odstraněny</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Saved message" xml:space="preserve">
<source>Saved message</source>
<note>message info title</note>
</trans-unit>
<trans-unit id="Scan QR code" xml:space="preserve">
<source>Scan QR code</source>
<target>Skenovat QR kód</target>
@@ -4298,6 +4413,14 @@ This is your link for group %@!</source>
<target>Hledat</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Search bar accepts invitation links." xml:space="preserve">
<source>Search bar accepts invitation links.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Search or paste SimpleX link" xml:space="preserve">
<source>Search or paste SimpleX link</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Secure queue" xml:space="preserve">
<source>Secure queue</source>
<target>Zabezpečit frontu</target>
@@ -4403,6 +4526,10 @@ This is your link for group %@!</source>
<target>Odeslat je z galerie nebo vlastní klávesnice.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Send up to 100 last messages to new members." xml:space="preserve">
<source>Send up to 100 last messages to new members.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Sender cancelled file transfer." xml:space="preserve">
<source>Sender cancelled file transfer.</source>
<target>Odesílatel zrušil přenos souboru.</target>
@@ -4572,9 +4699,8 @@ This is your link for group %@!</source>
<target>Sdílet odkaz</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Share one-time invitation link" xml:space="preserve">
<source>Share one-time invitation link</source>
<target>Jednorázový zvací odkaz</target>
<trans-unit id="Share this 1-time invite link" xml:space="preserve">
<source>Share this 1-time invite link</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Share with contacts" xml:space="preserve">
@@ -4697,16 +4823,15 @@ This is your link for group %@!</source>
<target>Někdo</target>
<note>notification title</note>
</trans-unit>
<trans-unit id="Start a new chat" xml:space="preserve">
<source>Start a new chat</source>
<target>Začít nový chat</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Start chat" xml:space="preserve">
<source>Start chat</source>
<target>Začít chat</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Start chat?" xml:space="preserve">
<source>Start chat?</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Start migration" xml:space="preserve">
<source>Start migration</source>
<target>Zahájit přenesení</target>
@@ -4831,6 +4956,14 @@ This is your link for group %@!</source>
<target>Klepnutím se připojíte inkognito</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Tap to paste link" xml:space="preserve">
<source>Tap to paste link</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Tap to scan" xml:space="preserve">
<source>Tap to scan</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Tap to start a new chat" xml:space="preserve">
<source>Tap to start a new chat</source>
<target>Klepnutím na zahájíte nový chat</target>
@@ -4893,6 +5026,10 @@ Může se to stát kvůli nějaké chybě, nebo pokud je spojení kompromitován
<target>Pokus o změnu přístupové fráze databáze nebyl dokončen.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="The code you scanned is not a SimpleX link QR code." xml:space="preserve">
<source>The code you scanned is not a SimpleX link QR code.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="The connection you accepted will be cancelled!" xml:space="preserve">
<source>The connection you accepted will be cancelled!</source>
<target>Připojení, které jste přijali, bude zrušeno!</target>
@@ -4958,21 +5095,15 @@ Může se to stát kvůli nějaké chybě, nebo pokud je spojení kompromitován
<target>Servery pro nová připojení vašeho aktuálního chat profilu **%@**.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="The text you pasted is not a SimpleX link." xml:space="preserve">
<source>The text you pasted is not a SimpleX link.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Theme" xml:space="preserve">
<source>Theme</source>
<target>Téma</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="There should be at least one user profile." xml:space="preserve">
<source>There should be at least one user profile.</source>
<target>Měl by tam být alespoň jeden uživatelský profil.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="There should be at least one visible user profile." xml:space="preserve">
<source>There should be at least one visible user profile.</source>
<target>Měl by tam být alespoň jeden viditelný uživatelský profil.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="These settings are for your current profile **%@**." xml:space="preserve">
<source>These settings are for your current profile **%@**.</source>
<target>Toto nastavení je pro váš aktuální profil **%@**.</target>
@@ -5002,6 +5133,10 @@ Může se to stát kvůli nějaké chybě, nebo pokud je spojení kompromitován
<source>This device name</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="This display name is invalid. Please choose another name." xml:space="preserve">
<source>This display name is invalid. Please choose another name.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="This group has over %lld members, delivery receipts are not sent." xml:space="preserve">
<source>This group has over %lld members, delivery receipts are not sent.</source>
<target>Tato skupina má více než %lld členů, potvrzení o doručení nejsou odesílány.</target>
@@ -5101,16 +5236,15 @@ Před zapnutím této funkce budete vyzváni k dokončení ověření.</target>
<target>Pokus o připojení k serveru používanému pro příjem zpráv od tohoto kontaktu.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Turkish interface" xml:space="preserve">
<source>Turkish interface</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Turn off" xml:space="preserve">
<source>Turn off</source>
<target>Vypnout</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Turn off notifications?" xml:space="preserve">
<source>Turn off notifications?</source>
<target>Vypnout upozornění?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Turn on" xml:space="preserve">
<source>Turn on</source>
<target>Zapnout</target>
@@ -5125,10 +5259,18 @@ Před zapnutím této funkce budete vyzváni k dokončení ověření.</target>
<source>Unblock</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Unblock for all" xml:space="preserve">
<source>Unblock for all</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Unblock member" xml:space="preserve">
<source>Unblock member</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Unblock member for all?" xml:space="preserve">
<source>Unblock member for all?</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Unblock member?" xml:space="preserve">
<source>Unblock member?</source>
<note>No comment provided by engineer.</note>
@@ -5223,6 +5365,10 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu
<target>Nepřečtený</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Up to 100 last messages are sent to new members." xml:space="preserve">
<source>Up to 100 last messages are sent to new members.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Update" xml:space="preserve">
<source>Update</source>
<target>Aktualizovat</target>
@@ -5307,6 +5453,10 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu
<target>Použít nový inkognito profil</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Use only local notifications?" xml:space="preserve">
<source>Use only local notifications?</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Use server" xml:space="preserve">
<source>Use server</source>
<target>Použít server</target>
@@ -5383,6 +5533,10 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu
<target>Zobrazení bezpečnostního kódu</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Visible history" xml:space="preserve">
<source>Visible history</source>
<note>chat feature</note>
</trans-unit>
<trans-unit id="Voice messages" xml:space="preserve">
<source>Voice messages</source>
<target>Hlasové zprávy</target>
@@ -5467,11 +5621,19 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu
<target>Pokud s někým sdílíte inkognito profil, bude tento profil použit pro skupiny, do kterých vás pozve.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="With encrypted files and media." xml:space="preserve">
<source>With encrypted files and media.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="With optional welcome message." xml:space="preserve">
<source>With optional welcome message.</source>
<target>S volitelnou uvítací zprávou.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="With reduced battery usage." xml:space="preserve">
<source>With reduced battery usage.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Wrong database passphrase" xml:space="preserve">
<source>Wrong database passphrase</source>
<target>Špatná přístupová fráze k databázi</target>
@@ -5556,11 +5718,6 @@ Repeat join request?</source>
<target>Můžete přijímat hovory z obrazovky zámku, bez ověření zařízení a aplikace.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button." xml:space="preserve">
<source>You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button.</source>
<target>Můžete se také připojit kliknutím na odkaz. Pokud se otevře v prohlížeči, klikněte na tlačítko **Otevřít v mobilní aplikaci**.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You can create it later" xml:space="preserve">
<source>You can create it later</source>
<target>Můžete vytvořit později</target>
@@ -5581,6 +5738,10 @@ Repeat join request?</source>
<target>Profil uživatele můžete skrýt nebo ztlumit - přejeďte prstem doprava.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You can make it visible to your SimpleX contacts via Settings." xml:space="preserve">
<source>You can make it visible to your SimpleX contacts via Settings.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You can now send messages to %@" xml:space="preserve">
<source>You can now send messages to %@</source>
<target>Nyní můžete posílat zprávy %@</target>
@@ -5621,6 +5782,10 @@ Repeat join request?</source>
<target>K formátování zpráv můžete použít markdown:</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You can view invitation link again in connection details." xml:space="preserve">
<source>You can view invitation link again in connection details.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You can't send messages!" xml:space="preserve">
<source>You can't send messages!</source>
<target>Nemůžete posílat zprávy!</target>
@@ -5805,13 +5970,6 @@ Toto připojení můžete zrušit a kontakt odebrat (a zkusit to později s nov
<target>Vaše kontakty mohou povolit úplné mazání zpráv.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your contacts in SimpleX will see it.&#10;You can change it in Settings." xml:space="preserve">
<source>Your contacts in SimpleX will see it.
You can change it in Settings.</source>
<target>Vaše kontakty v SimpleX ji uvidí.
Můžete ji změnit v Nastavení.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your contacts will remain connected." xml:space="preserve">
<source>Your contacts will remain connected.</source>
<target>Vaše kontakty zůstanou připojeny.</target>
@@ -5960,6 +6118,14 @@ Servery SimpleX nevidí váš profil.</target>
<source>blocked</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="blocked %@" xml:space="preserve">
<source>blocked %@</source>
<note>rcv group event chat item</note>
</trans-unit>
<trans-unit id="blocked by admin" xml:space="preserve">
<source>blocked by admin</source>
<note>blocked chat item</note>
</trans-unit>
<trans-unit id="bold" xml:space="preserve">
<source>bold</source>
<target>tučně</target>
@@ -6080,6 +6246,10 @@ Servery SimpleX nevidí váš profil.</target>
<target>připojení:%@</target>
<note>connection information</note>
</trans-unit>
<trans-unit id="contact %@ changed to %@" xml:space="preserve">
<source>contact %1$@ changed to %2$@</source>
<note>profile update event chat item</note>
</trans-unit>
<trans-unit id="contact has e2e encryption" xml:space="preserve">
<source>contact has e2e encryption</source>
<target>kontakt má šifrování e2e</target>
@@ -6348,6 +6518,10 @@ Servery SimpleX nevidí váš profil.</target>
<target>člen</target>
<note>member role</note>
</trans-unit>
<trans-unit id="member %@ changed to %@" xml:space="preserve">
<source>member %1$@ changed to %2$@</source>
<note>profile update event chat item</note>
</trans-unit>
<trans-unit id="member connected" xml:space="preserve">
<source>connected</source>
<target>připojeno</target>
@@ -6470,6 +6644,14 @@ Servery SimpleX nevidí váš profil.</target>
<target>odstraněno %@</target>
<note>rcv group event chat item</note>
</trans-unit>
<trans-unit id="removed contact address" xml:space="preserve">
<source>removed contact address</source>
<note>profile update event chat item</note>
</trans-unit>
<trans-unit id="removed profile picture" xml:space="preserve">
<source>removed profile picture</source>
<note>profile update event chat item</note>
</trans-unit>
<trans-unit id="removed you" xml:space="preserve">
<source>removed you</source>
<target>odstranil vás</target>
@@ -6500,6 +6682,14 @@ Servery SimpleX nevidí váš profil.</target>
<target>odeslat přímou zprávu</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="set new contact address" xml:space="preserve">
<source>set new contact address</source>
<note>profile update event chat item</note>
</trans-unit>
<trans-unit id="set new profile picture" xml:space="preserve">
<source>set new profile picture</source>
<note>profile update event chat item</note>
</trans-unit>
<trans-unit id="starting…" xml:space="preserve">
<source>starting…</source>
<target>začíná…</target>
@@ -6515,16 +6705,28 @@ Servery SimpleX nevidí váš profil.</target>
<target>tento kontakt</target>
<note>notification title</note>
</trans-unit>
<trans-unit id="unblocked %@" xml:space="preserve">
<source>unblocked %@</source>
<note>rcv group event chat item</note>
</trans-unit>
<trans-unit id="unknown" xml:space="preserve">
<source>unknown</source>
<target>neznámý</target>
<note>connection info</note>
</trans-unit>
<trans-unit id="unknown status" xml:space="preserve">
<source>unknown status</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="updated group profile" xml:space="preserve">
<source>updated group profile</source>
<target>aktualizoval profil skupiny</target>
<note>rcv group event chat item</note>
</trans-unit>
<trans-unit id="updated profile" xml:space="preserve">
<source>updated profile</source>
<note>profile update event chat item</note>
</trans-unit>
<trans-unit id="v%@" xml:space="preserve">
<source>v%@</source>
<note>No comment provided by engineer.</note>
@@ -6594,6 +6796,10 @@ Servery SimpleX nevidí váš profil.</target>
<target>jste pozorovatel</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="you blocked %@" xml:space="preserve">
<source>you blocked %@</source>
<note>snd group event chat item</note>
</trans-unit>
<trans-unit id="you changed address" xml:space="preserve">
<source>you changed address</source>
<target>změnili jste adresu</target>
@@ -6634,6 +6840,10 @@ Servery SimpleX nevidí váš profil.</target>
<target>sdíleli jste jednorázový odkaz inkognito</target>
<note>chat list item description</note>
</trans-unit>
<trans-unit id="you unblocked %@" xml:space="preserve">
<source>you unblocked %@</source>
<note>snd group event chat item</note>
</trans-unit>
<trans-unit id="you: " xml:space="preserve">
<source>you: </source>
<target>vy: </target>

View File

@@ -31,48 +31,59 @@ Available in v5.1</source>
<source> (</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id=" (can be copied)" xml:space="preserve">
<trans-unit id=" (can be copied)" xml:space="preserve" approved="no">
<source> (can be copied)</source>
<target state="translated"> (μπορεί να αντιγραφή)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="!1 colored!" xml:space="preserve">
<trans-unit id="!1 colored!" xml:space="preserve" approved="no">
<source>!1 colored!</source>
<target state="translated">!1 έγχρωμο!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="#secret#" xml:space="preserve">
<trans-unit id="#secret#" xml:space="preserve" approved="no">
<source>#secret#</source>
<target state="translated">#μυστικό#</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%@" xml:space="preserve">
<trans-unit id="%@" xml:space="preserve" approved="no">
<source>%@</source>
<target state="translated">%@</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%@ %@" xml:space="preserve">
<trans-unit id="%@ %@" xml:space="preserve" approved="no">
<source>%@ %@</source>
<target state="translated">%@ %@</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%@ / %@" xml:space="preserve">
<trans-unit id="%@ / %@" xml:space="preserve" approved="no">
<source>%@ / %@</source>
<target state="translated">%@ / %@</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%@ is connected!" xml:space="preserve">
<trans-unit id="%@ is connected!" xml:space="preserve" approved="no">
<source>%@ is connected!</source>
<target state="translated">%@ είναι συνδεδεμένο!</target>
<note>notification title</note>
</trans-unit>
<trans-unit id="%@ is not verified" xml:space="preserve">
<trans-unit id="%@ is not verified" xml:space="preserve" approved="no">
<source>%@ is not verified</source>
<target state="translated">%@ δεν είναι επαληθευμένο</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%@ is verified" xml:space="preserve">
<trans-unit id="%@ is verified" xml:space="preserve" approved="no">
<source>%@ is verified</source>
<target state="translated">%@ είναι επαληθευμένο</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%@ servers" xml:space="preserve">
<trans-unit id="%@ servers" xml:space="preserve" approved="no">
<source>%@ servers</source>
<target state="translated">%@ διακομιστές</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%@ wants to connect!" xml:space="preserve">
<trans-unit id="%@ wants to connect!" xml:space="preserve" approved="no">
<source>%@ wants to connect!</source>
<target state="translated">%@ θέλει να συνδεθεί!</target>
<note>notification title</note>
</trans-unit>
<trans-unit id="%d days" xml:space="preserve">
@@ -4162,6 +4173,66 @@ SimpleX servers cannot see your profile.</source>
<source>\~strike~</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%@ connected" xml:space="preserve" approved="no">
<source>%@ connected</source>
<target state="translated">%@ συνδεδεμένο</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="# %@" xml:space="preserve" approved="no">
<source># %@</source>
<target state="translated"># %@</target>
<note>copied message info title, # &lt;title&gt;</note>
</trans-unit>
<trans-unit id="%@ and %@" xml:space="preserve" approved="no">
<source>%@ and %@</source>
<target state="translated">%@ και %@</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%@ at %@:" xml:space="preserve" approved="no">
<source>%1$@ at %2$@:</source>
<target state="translated">%1$@ στις %2$@:</target>
<note>copied message info, &lt;sender&gt; at &lt;time&gt;</note>
</trans-unit>
<trans-unit id="## History" xml:space="preserve" approved="no">
<source>## History</source>
<target state="translated">## Ιστορικό</target>
<note>copied message info</note>
</trans-unit>
<trans-unit id="## In reply to" xml:space="preserve" approved="no">
<source>## In reply to</source>
<target state="translated">## Ως απαντηση σε</target>
<note>copied message info</note>
</trans-unit>
<trans-unit id="%@ (current)" xml:space="preserve" approved="no">
<source>%@ (current)</source>
<target state="translated">%@ (τωρινό)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%@ (current):" xml:space="preserve" approved="no">
<source>%@ (current):</source>
<target state="translated">%@ (τωρινό):</target>
<note>copied message info</note>
</trans-unit>
<trans-unit id="%@ and %@ connected" xml:space="preserve" approved="no">
<source>%@ and %@ connected</source>
<target state="translated">%@ και %@ συνδεδεμένο</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%@:" xml:space="preserve" approved="no">
<source>%@:</source>
<target state="translated">%@:</target>
<note>copied message info</note>
</trans-unit>
<trans-unit id="%@, %@ and %lld members" xml:space="preserve" approved="no">
<source>%@, %@ and %lld members</source>
<target state="translated">%@, %@ και %lld μέλη</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%@, %@ and %lld other members connected" xml:space="preserve" approved="no">
<source>%@, %@ and %lld other members connected</source>
<target state="translated">%@, %@ και %lld άλλα μέλη συνδέθηκαν</target>
<note>No comment provided by engineer.</note>
</trans-unit>
</body>
</file>
<file original="en.lproj/SimpleX--iOS--InfoPlist.strings" source-language="en" target-language="el" datatype="plaintext">

View File

@@ -212,6 +212,10 @@
<source>%lld messages blocked</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%lld messages blocked by admin" xml:space="preserve">
<source>%lld messages blocked by admin</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%lld messages marked deleted" xml:space="preserve">
<source>%lld messages marked deleted</source>
<note>No comment provided by engineer.</note>
@@ -303,14 +307,17 @@
<target>)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="**Add contact**: to create a new invitation link, or connect via a link you received." xml:space="preserve">
<source>**Add contact**: to create a new invitation link, or connect via a link you received.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="**Add new contact**: to create your one-time QR Code for your contact." xml:space="preserve">
<source>**Add new contact**: to create your one-time QR Code or link for your contact.</source>
<target>**Lisää uusi kontakti**: luo kertakäyttöinen QR-koodi tai linkki kontaktille.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="**Create link / QR code** for your contact to use." xml:space="preserve">
<source>**Create link / QR code** for your contact to use.</source>
<target>**Luo linkki / QR-koodi* kontaktille.</target>
<trans-unit id="**Create group**: to create a new group." xml:space="preserve">
<source>**Create group**: to create a new group.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have." xml:space="preserve">
@@ -323,11 +330,6 @@
<target>**Yksityisin**: älä käytä SimpleX Chat -ilmoituspalvelinta, tarkista viestit ajoittain taustalla (riippuu siitä, kuinka usein käytät sovellusta).</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="**Paste received link** or open it in the browser and tap **Open in mobile app**." xml:space="preserve">
<source>**Paste received link** or open it in the browser and tap **Open in mobile app**.</source>
<target>**Liitä vastaanotettu linkki** tai avaa se selaimessa ja napauta **Avaa mobiilisovelluksessa**.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="**Please note**: you will NOT be able to recover or change passphrase if you lose it." xml:space="preserve">
<source>**Please note**: you will NOT be able to recover or change passphrase if you lose it.</source>
<target>**Huomaa**: et voi palauttaa tai muuttaa tunnuslausetta, jos kadotat sen.</target>
@@ -338,11 +340,6 @@
<target>**Suositus**: laitetunnus ja ilmoitukset lähetetään SimpleX Chat -ilmoituspalvelimelle, mutta ei viestin sisältöä, kokoa tai sitä, keneltä se on peräisin.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="**Scan QR code**: to connect to your contact in person or via video call." xml:space="preserve">
<source>**Scan QR code**: to connect to your contact in person or via video call.</source>
<target>**Skannaa QR-koodi**: muodosta yhteys kontaktiisi henkilökohtaisesti tai videopuhelun kautta.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="**Warning**: Instant push notifications require passphrase saved in Keychain." xml:space="preserve">
<source>**Warning**: Instant push notifications require passphrase saved in Keychain.</source>
<target>**Varoitus**: Välittömät push-ilmoitukset vaativat tunnuslauseen, joka on tallennettu Keychainiin.</target>
@@ -437,11 +434,6 @@
<target>1 viikko</target>
<note>time interval</note>
</trans-unit>
<trans-unit id="1-time link" xml:space="preserve">
<source>1-time link</source>
<target>Kertakäyttölinkki</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="5 minutes" xml:space="preserve">
<source>5 minutes</source>
<target>5 minuuttia</target>
@@ -557,6 +549,10 @@
<target>Lisää osoite profiiliisi, jotta kontaktisi voivat jakaa sen muiden kanssa. Profiilipäivitys lähetetään kontakteillesi.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Add contact" xml:space="preserve">
<source>Add contact</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Add preset servers" xml:space="preserve">
<source>Add preset servers</source>
<target>Lisää esiasetettuja palvelimia</target>
@@ -627,6 +623,10 @@
<target>Kaikki ryhmän jäsenet pysyvät yhteydessä.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="All messages will be deleted - this cannot be undone!" xml:space="preserve">
<source>All messages will be deleted - this cannot be undone!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you." xml:space="preserve">
<source>All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you.</source>
<target>Kaikki viestit poistetaan - tätä ei voi kumota! Viestit poistuvat VAIN sinulta.</target>
@@ -661,9 +661,9 @@
<target>Salli katoavat viestit vain, jos kontaktisi sallii sen sinulle.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Allow irreversible message deletion only if your contact allows it to you." xml:space="preserve">
<source>Allow irreversible message deletion only if your contact allows it to you.</source>
<target>Salli peruuttamaton viestien poisto vain, jos kontaktisi sallii ne sinulle.</target>
<trans-unit id="Allow irreversible message deletion only if your contact allows it to you. (24 hours)" xml:space="preserve">
<source>Allow irreversible message deletion only if your contact allows it to you. (24 hours)</source>
<target>Salli peruuttamaton viestien poisto vain, jos kontaktisi sallii ne sinulle. (24 tuntia)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Allow message reactions only if your contact allows them." xml:space="preserve">
@@ -686,9 +686,9 @@
<target>Salli katoavien viestien lähettäminen.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Allow to irreversibly delete sent messages." xml:space="preserve">
<source>Allow to irreversibly delete sent messages.</source>
<target>Salli lähetettyjen viestien peruuttamaton poistaminen.</target>
<trans-unit id="Allow to irreversibly delete sent messages. (24 hours)" xml:space="preserve">
<source>Allow to irreversibly delete sent messages. (24 hours)</source>
<target>Salli lähetettyjen viestien peruuttamaton poistaminen. (24 tuntia)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Allow to send files and media." xml:space="preserve">
@@ -721,9 +721,9 @@
<target>Salli kontaktiesi soittaa sinulle.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Allow your contacts to irreversibly delete sent messages." xml:space="preserve">
<source>Allow your contacts to irreversibly delete sent messages.</source>
<target>Salli kontaktiesi poistaa lähetetyt viestit peruuttamattomasti.</target>
<trans-unit id="Allow your contacts to irreversibly delete sent messages. (24 hours)" xml:space="preserve">
<source>Allow your contacts to irreversibly delete sent messages. (24 hours)</source>
<target>Salli kontaktiesi poistaa lähetetyt viestit peruuttamattomasti. (24 tuntia)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Allow your contacts to send disappearing messages." xml:space="preserve">
@@ -895,6 +895,10 @@
<source>Block</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Block for all" xml:space="preserve">
<source>Block for all</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Block group members" xml:space="preserve">
<source>Block group members</source>
<note>No comment provided by engineer.</note>
@@ -903,18 +907,26 @@
<source>Block member</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Block member for all?" xml:space="preserve">
<source>Block member for all?</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Block member?" xml:space="preserve">
<source>Block member?</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Blocked by admin" xml:space="preserve">
<source>Blocked by admin</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Both you and your contact can add message reactions." xml:space="preserve">
<source>Both you and your contact can add message reactions.</source>
<target>Sekä sinä että kontaktisi voivat käyttää viestireaktioita.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Both you and your contact can irreversibly delete sent messages." xml:space="preserve">
<source>Both you and your contact can irreversibly delete sent messages.</source>
<target>Sekä sinä että kontaktisi voitte peruuttamattomasti poistaa lähetetyt viestit.</target>
<trans-unit id="Both you and your contact can irreversibly delete sent messages. (24 hours)" xml:space="preserve">
<source>Both you and your contact can irreversibly delete sent messages. (24 hours)</source>
<target>Sekä sinä että kontaktisi voitte peruuttamattomasti poistaa lähetetyt viestit. (24 tuntia)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Both you and your contact can make calls." xml:space="preserve">
@@ -951,9 +963,8 @@
<target>Puhelut</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Can't delete user profile!" xml:space="preserve">
<source>Can't delete user profile!</source>
<target>Käyttäjäprofiilia ei voi poistaa!</target>
<trans-unit id="Camera not available" xml:space="preserve">
<source>Camera not available</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Can't invite contact!" xml:space="preserve">
@@ -1067,6 +1078,10 @@
<target>Chat on pysäytetty</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Chat is stopped. If you already used this database on another device, you should transfer it back before starting chat." xml:space="preserve">
<source>Chat is stopped. If you already used this database on another device, you should transfer it back before starting chat.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Chat preferences" xml:space="preserve">
<source>Chat preferences</source>
<target>Chat-asetukset</target>
@@ -1112,6 +1127,10 @@
<target>Tyhjennä keskustelu?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Clear private notes?" xml:space="preserve">
<source>Clear private notes?</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Clear verification" xml:space="preserve">
<source>Clear verification</source>
<target>Tyhjennä vahvistus</target>
@@ -1203,11 +1222,6 @@ This is your own one-time link!</source>
<target>Yhdistä linkin kautta</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connect via link / QR code" xml:space="preserve">
<source>Connect via link / QR code</source>
<target>Yhdistä linkillä / QR-koodilla</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connect via one-time link" xml:space="preserve">
<source>Connect via one-time link</source>
<target>Yhdistä kertalinkillä</target>
@@ -1375,11 +1389,6 @@ This is your own one-time link!</source>
<target>Luo uusi profiili [työpöytäsovelluksessa](https://simplex.chat/downloads/). 💻</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Create one-time invitation link" xml:space="preserve">
<source>Create one-time invitation link</source>
<target>Luo kertakutsulinkki</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Create profile" xml:space="preserve">
<source>Create profile</source>
<note>No comment provided by engineer.</note>
@@ -1399,11 +1408,23 @@ This is your own one-time link!</source>
<target>Luo profiilisi</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Created at" xml:space="preserve">
<source>Created at</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Created at: %@" xml:space="preserve">
<source>Created at: %@</source>
<note>copied message info</note>
</trans-unit>
<trans-unit id="Created on %@" xml:space="preserve">
<source>Created on %@</source>
<target>Luotu %@</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Creating link…" xml:space="preserve">
<source>Creating link…</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Current Passcode" xml:space="preserve">
<source>Current Passcode</source>
<target>Nykyinen pääsykoodi</target>
@@ -1875,6 +1896,10 @@ This cannot be undone!</source>
<target>Tee myöhemmin</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Do not send history to new members." xml:space="preserve">
<source>Do not send history to new members.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Don't create address" xml:space="preserve">
<source>Don't create address</source>
<target>Älä luo osoitetta</target>
@@ -1945,6 +1970,10 @@ This cannot be undone!</source>
<target>Ota automaattinen viestien poisto käyttöön?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Enable camera access" xml:space="preserve">
<source>Enable camera access</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Enable for all" xml:space="preserve">
<source>Enable for all</source>
<target>Salli kaikille</target>
@@ -2009,6 +2038,10 @@ This cannot be undone!</source>
<target>Salattu viesti tai muu tapahtuma</target>
<note>notification</note>
</trans-unit>
<trans-unit id="Encrypted message: app is stopped" xml:space="preserve">
<source>Encrypted message: app is stopped</source>
<note>notification</note>
</trans-unit>
<trans-unit id="Encrypted message: database error" xml:space="preserve">
<source>Encrypted message: database error</source>
<target>Salattu viesti: tietokantavirhe</target>
@@ -2148,6 +2181,10 @@ This cannot be undone!</source>
<source>Error creating member contact</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error creating message" xml:space="preserve">
<source>Error creating message</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error creating profile!" xml:space="preserve">
<source>Error creating profile!</source>
<target>Virhe profiilin luomisessa!</target>
@@ -2233,6 +2270,10 @@ This cannot be undone!</source>
<target>Virhe %@-palvelimien lataamisessa</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error opening chat" xml:space="preserve">
<source>Error opening chat</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error receiving file" xml:space="preserve">
<source>Error receiving file</source>
<target>Virhe tiedoston vastaanottamisessa</target>
@@ -2273,6 +2314,10 @@ This cannot be undone!</source>
<target>Virhe käyttäjän salasanan tallentamisessa</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error scanning code: %@" xml:space="preserve">
<source>Error scanning code: %@</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error sending email" xml:space="preserve">
<source>Error sending email</source>
<target>Virhe sähköpostin lähettämisessä</target>
@@ -2596,9 +2641,9 @@ This cannot be undone!</source>
<target>Ryhmän jäsenet voivat lisätä viestireaktioita.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Group members can irreversibly delete sent messages." xml:space="preserve">
<source>Group members can irreversibly delete sent messages.</source>
<target>Ryhmän jäsenet voivat poistaa lähetetyt viestit peruuttamattomasti.</target>
<trans-unit id="Group members can irreversibly delete sent messages. (24 hours)" xml:space="preserve">
<source>Group members can irreversibly delete sent messages. (24 hours)</source>
<target>Ryhmän jäsenet voivat poistaa lähetetyt viestit peruuttamattomasti. (24 tuntia)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Group members can send direct messages." xml:space="preserve">
@@ -2706,6 +2751,10 @@ This cannot be undone!</source>
<target>Historia</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="History is not sent to new members." xml:space="preserve">
<source>History is not sent to new members.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="How SimpleX works" xml:space="preserve">
<source>How SimpleX works</source>
<target>Miten SimpleX toimii</target>
@@ -2741,11 +2790,6 @@ This cannot be undone!</source>
<target>Jos et voi tavata henkilökohtaisesti, näytä QR-koodi videopuhelussa tai jaa linkki.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="If you cannot meet in person, you can **scan QR code in the video call**, or your contact can share an invitation link." xml:space="preserve">
<source>If you cannot meet in person, you can **scan QR code in the video call**, or your contact can share an invitation link.</source>
<target>Jos et voi tavata henkilökohtaisesti, voit **skannata QR-koodin videopuhelussa** tai kontaktisi voi jakaa kutsulinkin.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="If you enter this passcode when opening the app, all app data will be irreversibly removed!" xml:space="preserve">
<source>If you enter this passcode when opening the app, all app data will be irreversibly removed!</source>
<target>Jos syötät tämän pääsykoodin sovellusta avatessasi, kaikki sovelluksen tiedot poistetaan peruuttamattomasti!</target>
@@ -2801,6 +2845,10 @@ This cannot be undone!</source>
<target>Tuo tietokanta</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Improved message delivery" xml:space="preserve">
<source>Improved message delivery</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Improved privacy and security" xml:space="preserve">
<source>Improved privacy and security</source>
<target>Parannettu yksityisyys ja turvallisuus</target>
@@ -2901,15 +2949,31 @@ This cannot be undone!</source>
<target>Käyttöliittymä</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Invalid QR code" xml:space="preserve">
<source>Invalid QR code</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Invalid connection link" xml:space="preserve">
<source>Invalid connection link</source>
<target>Virheellinen yhteyslinkki</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Invalid display name!" xml:space="preserve">
<source>Invalid display name!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Invalid link" xml:space="preserve">
<source>Invalid link</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Invalid name!" xml:space="preserve">
<source>Invalid name!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Invalid response" xml:space="preserve">
<source>Invalid response</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Invalid server address!" xml:space="preserve">
<source>Invalid server address!</source>
<target>Virheellinen palvelinosoite!</target>
@@ -3001,6 +3065,10 @@ This cannot be undone!</source>
<target>Liity ryhmään</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Join group conversations" xml:space="preserve">
<source>Join group conversations</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Join group?" xml:space="preserve">
<source>Join group?</source>
<note>No comment provided by engineer.</note>
@@ -3024,10 +3092,18 @@ This is your link for group %@!</source>
<target>Liittyy ryhmään</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Keep" xml:space="preserve">
<source>Keep</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Keep the app open to use it from desktop" xml:space="preserve">
<source>Keep the app open to use it from desktop</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Keep unused invitation?" xml:space="preserve">
<source>Keep unused invitation?</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Keep your connections" xml:space="preserve">
<source>Keep your connections</source>
<target>Pidä kontaktisi</target>
@@ -3110,6 +3186,11 @@ This is your link for group %@!</source>
<target>Live-viestit</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Local" xml:space="preserve">
<source>Local</source>
<target>Paikallinen</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Local name" xml:space="preserve">
<source>Local name</source>
<target>Paikallinen nimi</target>
@@ -3349,6 +3430,10 @@ This is your link for group %@!</source>
<target>Uusi pääsykoodi</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="New chat" xml:space="preserve">
<source>New chat</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="New contact request" xml:space="preserve">
<source>New contact request</source>
<target>Uusi kontaktipyyntö</target>
@@ -3471,16 +3556,15 @@ This is your link for group %@!</source>
- poista jäsenet käytöstä ("tarkkailija" rooli)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="OK" xml:space="preserve">
<source>OK</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Off" xml:space="preserve">
<source>Off</source>
<target>Pois</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Off (Local)" xml:space="preserve">
<source>Off (Local)</source>
<target>Pois (Paikallinen)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Ok" xml:space="preserve">
<source>Ok</source>
<target>Ok</target>
@@ -3541,9 +3625,9 @@ This is your link for group %@!</source>
<target>Vain sinä voit lisätä viestireaktioita.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Only you can irreversibly delete messages (your contact can mark them for deletion)." xml:space="preserve">
<source>Only you can irreversibly delete messages (your contact can mark them for deletion).</source>
<target>Vain sinä voit poistaa viestejä peruuttamattomasti (kontaktisi voi merkitä ne poistettavaksi).</target>
<trans-unit id="Only you can irreversibly delete messages (your contact can mark them for deletion). (24 hours)" xml:space="preserve">
<source>Only you can irreversibly delete messages (your contact can mark them for deletion). (24 hours)</source>
<target>Vain sinä voit poistaa viestejä peruuttamattomasti (kontaktisi voi merkitä ne poistettavaksi). (24 tuntia)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Only you can make calls." xml:space="preserve">
@@ -3566,9 +3650,9 @@ This is your link for group %@!</source>
<target>Vain kontaktisi voi lisätä viestireaktioita.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Only your contact can irreversibly delete messages (you can mark them for deletion)." xml:space="preserve">
<source>Only your contact can irreversibly delete messages (you can mark them for deletion).</source>
<target>Vain kontaktisi voi poistaa viestejä peruuttamattomasti (voit merkitä ne poistettavaksi).</target>
<trans-unit id="Only your contact can irreversibly delete messages (you can mark them for deletion). (24 hours)" xml:space="preserve">
<source>Only your contact can irreversibly delete messages (you can mark them for deletion). (24 hours)</source>
<target>Vain kontaktisi voi poistaa viestejä peruuttamattomasti (voit merkitä ne poistettavaksi). (24 tuntia)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Only your contact can make calls." xml:space="preserve">
@@ -3619,9 +3703,16 @@ This is your link for group %@!</source>
<target>Avoimen lähdekoodin protokolla ja koodi - kuka tahansa voi käyttää palvelimia.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Opening database…" xml:space="preserve">
<source>Opening database…</source>
<target>Avataan tietokantaa…</target>
<trans-unit id="Opening app…" xml:space="preserve">
<source>Opening app…</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Or scan QR code" xml:space="preserve">
<source>Or scan QR code</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Or show this code" xml:space="preserve">
<source>Or show this code</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="PING count" xml:space="preserve">
@@ -3664,10 +3755,9 @@ This is your link for group %@!</source>
<target>Salasana näytettäväksi</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Paste" xml:space="preserve">
<source>Paste</source>
<target>Liitä</target>
<note>No comment provided by engineer.</note>
<trans-unit id="Past member %@" xml:space="preserve">
<source>Past member %@</source>
<note>past/unknown group member</note>
</trans-unit>
<trans-unit id="Paste desktop address" xml:space="preserve">
<source>Paste desktop address</source>
@@ -3678,15 +3768,13 @@ This is your link for group %@!</source>
<target>Liitä kuva</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Paste received link" xml:space="preserve">
<source>Paste received link</source>
<target>Liitä vastaanotettu linkki</target>
<trans-unit id="Paste link to connect!" xml:space="preserve">
<source>Paste link to connect!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Paste the link you received to connect with your contact." xml:space="preserve">
<source>Paste the link you received to connect with your contact.</source>
<target>Liitä saamasi linkki, jonka avulla voit muodostaa yhteyden kontaktiisi.</target>
<note>placeholder</note>
<trans-unit id="Paste the link you received" xml:space="preserve">
<source>Paste the link you received</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="People can connect to you only via the links you share." xml:space="preserve">
<source>People can connect to you only via the links you share.</source>
@@ -3723,6 +3811,11 @@ This is your link for group %@!</source>
<target>Tarkista omasi ja kontaktin asetukset.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Please contact developers.&#10;Error: %@" xml:space="preserve">
<source>Please contact developers.
Error: %@</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Please contact group admin." xml:space="preserve">
<source>Please contact group admin.</source>
<target>Ota yhteyttä ryhmän ylläpitäjään.</target>
@@ -3808,6 +3901,10 @@ This is your link for group %@!</source>
<target>Yksityiset tiedostonimet</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Private notes" xml:space="preserve">
<source>Private notes</source>
<note>name of notes to self</note>
</trans-unit>
<trans-unit id="Profile and server connections" xml:space="preserve">
<source>Profile and server connections</source>
<target>Profiili- ja palvelinyhteydet</target>
@@ -3926,6 +4023,10 @@ This is your link for group %@!</source>
<target>Lue lisää [Käyttöoppaasta](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address).</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)." xml:space="preserve">
<source>Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode).</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends)." xml:space="preserve">
<source>Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends).</source>
<target>Lue lisää [Käyttöoppaasta](https://simplex.chat/docs/guide/readme.html#connect-to-friends).</target>
@@ -3981,6 +4082,10 @@ This is your link for group %@!</source>
<target>Vastaanotto kautta</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Recent history and improved [directory bot](simplex:/contact#/?v=1-4&amp;smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion)." xml:space="preserve">
<source>Recent history and improved [directory bot](simplex:/contact#/?v=1-4&amp;smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion).</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Recipients see updates as you type them." xml:space="preserve">
<source>Recipients see updates as you type them.</source>
<target>Vastaanottajat näkevät päivitykset, kun kirjoitat niitä.</target>
@@ -4134,6 +4239,10 @@ This is your link for group %@!</source>
<target>Virhe tietokannan palauttamisessa</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Retry" xml:space="preserve">
<source>Retry</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Reveal" xml:space="preserve">
<source>Reveal</source>
<target>Paljasta</target>
@@ -4259,6 +4368,10 @@ This is your link for group %@!</source>
<target>Tallennetut WebRTC ICE -palvelimet poistetaan</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Saved message" xml:space="preserve">
<source>Saved message</source>
<note>message info title</note>
</trans-unit>
<trans-unit id="Scan QR code" xml:space="preserve">
<source>Scan QR code</source>
<target>Skannaa QR-koodi</target>
@@ -4288,6 +4401,14 @@ This is your link for group %@!</source>
<target>Haku</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Search bar accepts invitation links." xml:space="preserve">
<source>Search bar accepts invitation links.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Search or paste SimpleX link" xml:space="preserve">
<source>Search or paste SimpleX link</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Secure queue" xml:space="preserve">
<source>Secure queue</source>
<target>Turvallinen jono</target>
@@ -4392,6 +4513,10 @@ This is your link for group %@!</source>
<target>Lähetä ne galleriasta tai mukautetuista näppäimistöistä.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Send up to 100 last messages to new members." xml:space="preserve">
<source>Send up to 100 last messages to new members.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Sender cancelled file transfer." xml:space="preserve">
<source>Sender cancelled file transfer.</source>
<target>Lähettäjä peruutti tiedoston siirron.</target>
@@ -4561,9 +4686,8 @@ This is your link for group %@!</source>
<target>Jaa linkki</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Share one-time invitation link" xml:space="preserve">
<source>Share one-time invitation link</source>
<target>Jaa kertakutsulinkki</target>
<trans-unit id="Share this 1-time invite link" xml:space="preserve">
<source>Share this 1-time invite link</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Share with contacts" xml:space="preserve">
@@ -4685,16 +4809,15 @@ This is your link for group %@!</source>
<target>Joku</target>
<note>notification title</note>
</trans-unit>
<trans-unit id="Start a new chat" xml:space="preserve">
<source>Start a new chat</source>
<target>Aloita uusi keskustelu</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Start chat" xml:space="preserve">
<source>Start chat</source>
<target>Aloita keskustelu</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Start chat?" xml:space="preserve">
<source>Start chat?</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Start migration" xml:space="preserve">
<source>Start migration</source>
<target>Aloita siirto</target>
@@ -4819,6 +4942,14 @@ This is your link for group %@!</source>
<target>Napauta liittyäksesi incognito-tilassa</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Tap to paste link" xml:space="preserve">
<source>Tap to paste link</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Tap to scan" xml:space="preserve">
<source>Tap to scan</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Tap to start a new chat" xml:space="preserve">
<source>Tap to start a new chat</source>
<target>Aloita uusi keskustelu napauttamalla</target>
@@ -4881,6 +5012,10 @@ Tämä voi johtua jostain virheestä tai siitä, että yhteys on vaarantunut.</t
<target>Tietokannan tunnuslauseen muuttamista ei suoritettu loppuun.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="The code you scanned is not a SimpleX link QR code." xml:space="preserve">
<source>The code you scanned is not a SimpleX link QR code.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="The connection you accepted will be cancelled!" xml:space="preserve">
<source>The connection you accepted will be cancelled!</source>
<target>Hyväksymäsi yhteys peruuntuu!</target>
@@ -4946,21 +5081,15 @@ Tämä voi johtua jostain virheestä tai siitä, että yhteys on vaarantunut.</t
<target>Palvelimet nykyisen keskusteluprofiilisi uusille yhteyksille **%@**.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="The text you pasted is not a SimpleX link." xml:space="preserve">
<source>The text you pasted is not a SimpleX link.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Theme" xml:space="preserve">
<source>Theme</source>
<target>Teema</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="There should be at least one user profile." xml:space="preserve">
<source>There should be at least one user profile.</source>
<target>Käyttäjäprofiileja tulee olla vähintään yksi.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="There should be at least one visible user profile." xml:space="preserve">
<source>There should be at least one visible user profile.</source>
<target>Näkyviä käyttäjäprofiileja tulee olla vähintään yksi.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="These settings are for your current profile **%@**." xml:space="preserve">
<source>These settings are for your current profile **%@**.</source>
<target>Nämä asetukset koskevat nykyistä profiiliasi **%@**.</target>
@@ -4990,6 +5119,10 @@ Tämä voi johtua jostain virheestä tai siitä, että yhteys on vaarantunut.</t
<source>This device name</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="This display name is invalid. Please choose another name." xml:space="preserve">
<source>This display name is invalid. Please choose another name.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="This group has over %lld members, delivery receipts are not sent." xml:space="preserve">
<source>This group has over %lld members, delivery receipts are not sent.</source>
<target>Tässä ryhmässä on yli %lld jäsentä, lähetyskuittauksia ei lähetetä.</target>
@@ -5088,16 +5221,15 @@ Sinua kehotetaan suorittamaan todennus loppuun, ennen kuin tämä ominaisuus ote
<target>Yritetään muodostaa yhteys palvelimeen, jota käytetään viestien vastaanottamiseen tältä kontaktilta.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Turkish interface" xml:space="preserve">
<source>Turkish interface</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Turn off" xml:space="preserve">
<source>Turn off</source>
<target>Sammuta</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Turn off notifications?" xml:space="preserve">
<source>Turn off notifications?</source>
<target>Kytke ilmoitukset pois päältä?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Turn on" xml:space="preserve">
<source>Turn on</source>
<target>Kytke päälle</target>
@@ -5112,10 +5244,18 @@ Sinua kehotetaan suorittamaan todennus loppuun, ennen kuin tämä ominaisuus ote
<source>Unblock</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Unblock for all" xml:space="preserve">
<source>Unblock for all</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Unblock member" xml:space="preserve">
<source>Unblock member</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Unblock member for all?" xml:space="preserve">
<source>Unblock member for all?</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Unblock member?" xml:space="preserve">
<source>Unblock member?</source>
<note>No comment provided by engineer.</note>
@@ -5210,6 +5350,10 @@ Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja
<target>Lukematon</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Up to 100 last messages are sent to new members." xml:space="preserve">
<source>Up to 100 last messages are sent to new members.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Update" xml:space="preserve">
<source>Update</source>
<target>Päivitä</target>
@@ -5294,6 +5438,10 @@ Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja
<target>Käytä uutta incognito-profiilia</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Use only local notifications?" xml:space="preserve">
<source>Use only local notifications?</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Use server" xml:space="preserve">
<source>Use server</source>
<target>Käytä palvelinta</target>
@@ -5370,6 +5518,10 @@ Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja
<target>Näytä turvakoodi</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Visible history" xml:space="preserve">
<source>Visible history</source>
<note>chat feature</note>
</trans-unit>
<trans-unit id="Voice messages" xml:space="preserve">
<source>Voice messages</source>
<target>Ääniviestit</target>
@@ -5454,11 +5606,19 @@ Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja
<target>Kun jaat inkognitoprofiilin jonkun kanssa, tätä profiilia käytetään ryhmissä, joihin tämä sinut kutsuu.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="With encrypted files and media." xml:space="preserve">
<source>With encrypted files and media.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="With optional welcome message." xml:space="preserve">
<source>With optional welcome message.</source>
<target>Valinnaisella tervetuloviestillä.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="With reduced battery usage." xml:space="preserve">
<source>With reduced battery usage.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Wrong database passphrase" xml:space="preserve">
<source>Wrong database passphrase</source>
<target>Väärä tietokannan tunnuslause</target>
@@ -5543,11 +5703,6 @@ Repeat join request?</source>
<target>Voit vastaanottaa puheluita lukitusnäytöltä ilman laitteen ja sovelluksen todennusta.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button." xml:space="preserve">
<source>You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button.</source>
<target>Voit myös muodostaa yhteyden klikkaamalla linkkiä. Jos se avautuu selaimessa, napsauta **Avaa mobiilisovelluksessa**-painiketta.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You can create it later" xml:space="preserve">
<source>You can create it later</source>
<target>Voit luoda sen myöhemmin</target>
@@ -5568,6 +5723,10 @@ Repeat join request?</source>
<target>Voit piilottaa tai mykistää käyttäjäprofiilin pyyhkäisemällä sitä oikealle.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You can make it visible to your SimpleX contacts via Settings." xml:space="preserve">
<source>You can make it visible to your SimpleX contacts via Settings.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You can now send messages to %@" xml:space="preserve">
<source>You can now send messages to %@</source>
<target>Voit nyt lähettää viestejä %@:lle</target>
@@ -5608,6 +5767,10 @@ Repeat join request?</source>
<target>Voit käyttää markdownia viestien muotoiluun:</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You can view invitation link again in connection details." xml:space="preserve">
<source>You can view invitation link again in connection details.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You can't send messages!" xml:space="preserve">
<source>You can't send messages!</source>
<target>Et voi lähettää viestejä!</target>
@@ -5792,13 +5955,6 @@ Voit peruuttaa tämän yhteyden ja poistaa kontaktin (ja yrittää myöhemmin uu
<target>Kontaktisi voivat sallia viestien täydellisen poistamisen.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your contacts in SimpleX will see it.&#10;You can change it in Settings." xml:space="preserve">
<source>Your contacts in SimpleX will see it.
You can change it in Settings.</source>
<target>Kontaktisi SimpleX:ssä näkevät sen.
Voit muuttaa sitä Asetuksista.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your contacts will remain connected." xml:space="preserve">
<source>Your contacts will remain connected.</source>
<target>Kontaktisi pysyvät yhdistettyinä.</target>
@@ -5947,6 +6103,14 @@ SimpleX-palvelimet eivät näe profiiliasi.</target>
<source>blocked</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="blocked %@" xml:space="preserve">
<source>blocked %@</source>
<note>rcv group event chat item</note>
</trans-unit>
<trans-unit id="blocked by admin" xml:space="preserve">
<source>blocked by admin</source>
<note>blocked chat item</note>
</trans-unit>
<trans-unit id="bold" xml:space="preserve">
<source>bold</source>
<target>lihavoitu</target>
@@ -6066,6 +6230,10 @@ SimpleX-palvelimet eivät näe profiiliasi.</target>
<target>yhteys:%@</target>
<note>connection information</note>
</trans-unit>
<trans-unit id="contact %@ changed to %@" xml:space="preserve">
<source>contact %1$@ changed to %2$@</source>
<note>profile update event chat item</note>
</trans-unit>
<trans-unit id="contact has e2e encryption" xml:space="preserve">
<source>contact has e2e encryption</source>
<target>kontaktilla on e2e-salaus</target>
@@ -6335,6 +6503,10 @@ SimpleX-palvelimet eivät näe profiiliasi.</target>
<target>jäsen</target>
<note>member role</note>
</trans-unit>
<trans-unit id="member %@ changed to %@" xml:space="preserve">
<source>member %1$@ changed to %2$@</source>
<note>profile update event chat item</note>
</trans-unit>
<trans-unit id="member connected" xml:space="preserve">
<source>connected</source>
<target>yhdistetty</target>
@@ -6457,6 +6629,14 @@ SimpleX-palvelimet eivät näe profiiliasi.</target>
<target>%@ poistettu</target>
<note>rcv group event chat item</note>
</trans-unit>
<trans-unit id="removed contact address" xml:space="preserve">
<source>removed contact address</source>
<note>profile update event chat item</note>
</trans-unit>
<trans-unit id="removed profile picture" xml:space="preserve">
<source>removed profile picture</source>
<note>profile update event chat item</note>
</trans-unit>
<trans-unit id="removed you" xml:space="preserve">
<source>removed you</source>
<target>poisti sinut</target>
@@ -6486,6 +6666,14 @@ SimpleX-palvelimet eivät näe profiiliasi.</target>
<source>send direct message</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="set new contact address" xml:space="preserve">
<source>set new contact address</source>
<note>profile update event chat item</note>
</trans-unit>
<trans-unit id="set new profile picture" xml:space="preserve">
<source>set new profile picture</source>
<note>profile update event chat item</note>
</trans-unit>
<trans-unit id="starting…" xml:space="preserve">
<source>starting…</source>
<target>alkaa…</target>
@@ -6501,16 +6689,28 @@ SimpleX-palvelimet eivät näe profiiliasi.</target>
<target>tämä kontakti</target>
<note>notification title</note>
</trans-unit>
<trans-unit id="unblocked %@" xml:space="preserve">
<source>unblocked %@</source>
<note>rcv group event chat item</note>
</trans-unit>
<trans-unit id="unknown" xml:space="preserve">
<source>unknown</source>
<target>tuntematon</target>
<note>connection info</note>
</trans-unit>
<trans-unit id="unknown status" xml:space="preserve">
<source>unknown status</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="updated group profile" xml:space="preserve">
<source>updated group profile</source>
<target>päivitetty ryhmäprofiili</target>
<note>rcv group event chat item</note>
</trans-unit>
<trans-unit id="updated profile" xml:space="preserve">
<source>updated profile</source>
<note>profile update event chat item</note>
</trans-unit>
<trans-unit id="v%@" xml:space="preserve">
<source>v%@</source>
<note>No comment provided by engineer.</note>
@@ -6580,6 +6780,10 @@ SimpleX-palvelimet eivät näe profiiliasi.</target>
<target>olet tarkkailija</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="you blocked %@" xml:space="preserve">
<source>you blocked %@</source>
<note>snd group event chat item</note>
</trans-unit>
<trans-unit id="you changed address" xml:space="preserve">
<source>you changed address</source>
<target>muutit osoitetta</target>
@@ -6620,6 +6824,10 @@ SimpleX-palvelimet eivät näe profiiliasi.</target>
<target>jaoit kertalinkin incognito-tilassa</target>
<note>chat list item description</note>
</trans-unit>
<trans-unit id="you unblocked %@" xml:space="preserve">
<source>you unblocked %@</source>
<note>snd group event chat item</note>
</trans-unit>
<trans-unit id="you: " xml:space="preserve">
<source>you: </source>
<target>sinä: </target>

View File

@@ -207,6 +207,10 @@
<source>%lld messages blocked</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%lld messages blocked by admin" xml:space="preserve">
<source>%lld messages blocked by admin</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%lld messages marked deleted" xml:space="preserve">
<source>%lld messages marked deleted</source>
<note>No comment provided by engineer.</note>
@@ -297,14 +301,17 @@
<target>)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="**Add contact**: to create a new invitation link, or connect via a link you received." xml:space="preserve">
<source>**Add contact**: to create a new invitation link, or connect via a link you received.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="**Add new contact**: to create your one-time QR Code for your contact." xml:space="preserve">
<source>**Add new contact**: to create your one-time QR Code or link for your contact.</source>
<target>**เพิ่มผู้ติดต่อใหม่**: เพื่อสร้างคิวอาร์โค้ดแบบใช้ครั้งเดียวหรือลิงก์สำหรับผู้ติดต่อของคุณ</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="**Create link / QR code** for your contact to use." xml:space="preserve">
<source>**Create link / QR code** for your contact to use.</source>
<target>**สร้างลิงค์ / คิวอาร์โค้ด** เพื่อให้ผู้ติดต่อของคุณใช้</target>
<trans-unit id="**Create group**: to create a new group." xml:space="preserve">
<source>**Create group**: to create a new group.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have." xml:space="preserve">
@@ -317,11 +324,6 @@
<target>**ส่วนตัวที่สุด**: ไม่ใช้เซิร์ฟเวอร์การแจ้งเตือนของ SimpleX Chat ตรวจสอบข้อความเป็นระยะในพื้นหลัง (ขึ้นอยู่กับความถี่ที่คุณใช้แอป)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="**Paste received link** or open it in the browser and tap **Open in mobile app**." xml:space="preserve">
<source>**Paste received link** or open it in the browser and tap **Open in mobile app**.</source>
<target>**แปะลิงก์ที่ได้รับ** หรือเปิดในเบราว์เซอร์แล้วแตะ **เปิดในแอปมือถือ**</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="**Please note**: you will NOT be able to recover or change passphrase if you lose it." xml:space="preserve">
<source>**Please note**: you will NOT be able to recover or change passphrase if you lose it.</source>
<target>**โปรดทราบ**: คุณจะไม่สามารถกู้คืนหรือเปลี่ยนรหัสผ่านได้หากคุณทำรหัสผ่านหาย</target>
@@ -332,11 +334,6 @@
<target>**แนะนำ**: โทเค็นอุปกรณ์และการแจ้งเตือนจะถูกส่งไปยังเซิร์ฟเวอร์การแจ้งเตือนของ SimpleX Chat แต่ไม่ใช่เนื้อหาข้อความ ขนาด หรือผู้ที่ส่ง</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="**Scan QR code**: to connect to your contact in person or via video call." xml:space="preserve">
<source>**Scan QR code**: to connect to your contact in person or via video call.</source>
<target>**สแกนคิวอาร์โค้ด**: เพื่อเชื่อมต่อกับผู้ติดต่อของคุณด้วยตนเองหรือผ่านการสนทนาทางวิดีโอ</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="**Warning**: Instant push notifications require passphrase saved in Keychain." xml:space="preserve">
<source>**Warning**: Instant push notifications require passphrase saved in Keychain.</source>
<target>**คำเตือน**: การแจ้งเตือนแบบพุชทันทีจำเป็นต้องบันทึกรหัสผ่านไว้ใน Keychain</target>
@@ -431,11 +428,6 @@
<target>1 สัปดาห์</target>
<note>time interval</note>
</trans-unit>
<trans-unit id="1-time link" xml:space="preserve">
<source>1-time link</source>
<target>ลิงก์สำหรับใช้ 1 ครั้ง</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="5 minutes" xml:space="preserve">
<source>5 minutes</source>
<target>5 นาที</target>
@@ -549,6 +541,10 @@
<target>เพิ่มที่อยู่ลงในโปรไฟล์ของคุณ เพื่อให้ผู้ติดต่อของคุณสามารถแชร์กับผู้อื่นได้ การอัปเดตโปรไฟล์จะถูกส่งไปยังผู้ติดต่อของคุณ</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Add contact" xml:space="preserve">
<source>Add contact</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Add preset servers" xml:space="preserve">
<source>Add preset servers</source>
<target>เพิ่มเซิร์ฟเวอร์ที่ตั้งไว้ล่วงหน้า</target>
@@ -619,6 +615,10 @@
<target>สมาชิกในกลุ่มทุกคนจะยังคงเชื่อมต่ออยู่.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="All messages will be deleted - this cannot be undone!" xml:space="preserve">
<source>All messages will be deleted - this cannot be undone!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you." xml:space="preserve">
<source>All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you.</source>
<target>ข้อความทั้งหมดจะถูกลบ - ไม่สามารถยกเลิกได้! ข้อความจะถูกลบสำหรับคุณเท่านั้น.</target>
@@ -653,8 +653,8 @@
<target>อนุญาตให้ข้อความที่หายไปเฉพาะในกรณีที่ผู้ติดต่อของคุณอนุญาตเท่านั้น.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Allow irreversible message deletion only if your contact allows it to you." xml:space="preserve">
<source>Allow irreversible message deletion only if your contact allows it to you.</source>
<trans-unit id="Allow irreversible message deletion only if your contact allows it to you. (24 hours)" xml:space="preserve">
<source>Allow irreversible message deletion only if your contact allows it to you. (24 hours)</source>
<target>อนุญาตให้ลบข้อความแบบถาวรเฉพาะในกรณีที่ผู้ติดต่อของคุณอนุญาตให้คุณเท่านั้น</target>
<note>No comment provided by engineer.</note>
</trans-unit>
@@ -678,8 +678,8 @@
<target>อนุญาตให้ส่งข้อความที่จะหายไปหลังปิดแชท (disappearing message)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Allow to irreversibly delete sent messages." xml:space="preserve">
<source>Allow to irreversibly delete sent messages.</source>
<trans-unit id="Allow to irreversibly delete sent messages. (24 hours)" xml:space="preserve">
<source>Allow to irreversibly delete sent messages. (24 hours)</source>
<target>อนุญาตให้ลบข้อความที่ส่งไปแล้วอย่างถาวร</target>
<note>No comment provided by engineer.</note>
</trans-unit>
@@ -713,8 +713,8 @@
<target>อนุญาตให้ผู้ติดต่อของคุณโทรหาคุณ</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Allow your contacts to irreversibly delete sent messages." xml:space="preserve">
<source>Allow your contacts to irreversibly delete sent messages.</source>
<trans-unit id="Allow your contacts to irreversibly delete sent messages. (24 hours)" xml:space="preserve">
<source>Allow your contacts to irreversibly delete sent messages. (24 hours)</source>
<target>อนุญาตให้ผู้ติดต่อของคุณลบข้อความที่ส่งแล้วอย่างถาวร</target>
<note>No comment provided by engineer.</note>
</trans-unit>
@@ -887,6 +887,10 @@
<source>Block</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Block for all" xml:space="preserve">
<source>Block for all</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Block group members" xml:space="preserve">
<source>Block group members</source>
<note>No comment provided by engineer.</note>
@@ -895,17 +899,25 @@
<source>Block member</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Block member for all?" xml:space="preserve">
<source>Block member for all?</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Block member?" xml:space="preserve">
<source>Block member?</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Blocked by admin" xml:space="preserve">
<source>Blocked by admin</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Both you and your contact can add message reactions." xml:space="preserve">
<source>Both you and your contact can add message reactions.</source>
<target>ทั้งคุณและผู้ติดต่อของคุณสามารถเพิ่มปฏิกิริยาของข้อความได้</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Both you and your contact can irreversibly delete sent messages." xml:space="preserve">
<source>Both you and your contact can irreversibly delete sent messages.</source>
<trans-unit id="Both you and your contact can irreversibly delete sent messages. (24 hours)" xml:space="preserve">
<source>Both you and your contact can irreversibly delete sent messages. (24 hours)</source>
<target>ทั้งคุณและผู้ติดต่อของคุณสามารถลบข้อความที่ส่งแล้วอย่างถาวรได้</target>
<note>No comment provided by engineer.</note>
</trans-unit>
@@ -943,9 +955,8 @@
<target>โทร</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Can't delete user profile!" xml:space="preserve">
<source>Can't delete user profile!</source>
<target>ไม่สามารถลบโปรไฟล์ผู้ใช้ได้!</target>
<trans-unit id="Camera not available" xml:space="preserve">
<source>Camera not available</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Can't invite contact!" xml:space="preserve">
@@ -1059,6 +1070,10 @@
<target>การแชทหยุดทํางานแล้ว</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Chat is stopped. If you already used this database on another device, you should transfer it back before starting chat." xml:space="preserve">
<source>Chat is stopped. If you already used this database on another device, you should transfer it back before starting chat.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Chat preferences" xml:space="preserve">
<source>Chat preferences</source>
<target>ค่ากําหนดในการแชท</target>
@@ -1104,6 +1119,10 @@
<target>ลบการสนทนา?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Clear private notes?" xml:space="preserve">
<source>Clear private notes?</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Clear verification" xml:space="preserve">
<source>Clear verification</source>
<target>ล้างการยืนยัน</target>
@@ -1194,11 +1213,6 @@ This is your own one-time link!</source>
<target>เชื่อมต่อผ่านลิงก์</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connect via link / QR code" xml:space="preserve">
<source>Connect via link / QR code</source>
<target>เชื่อมต่อผ่านลิงค์ / คิวอาร์โค้ด</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connect via one-time link" xml:space="preserve">
<source>Connect via one-time link</source>
<note>No comment provided by engineer.</note>
@@ -1364,11 +1378,6 @@ This is your own one-time link!</source>
<source>Create new profile in [desktop app](https://simplex.chat/downloads/). 💻</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Create one-time invitation link" xml:space="preserve">
<source>Create one-time invitation link</source>
<target>สร้างลิงก์เชิญแบบใช้ครั้งเดียว</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Create profile" xml:space="preserve">
<source>Create profile</source>
<note>No comment provided by engineer.</note>
@@ -1388,11 +1397,23 @@ This is your own one-time link!</source>
<target>สร้างโปรไฟล์ของคุณ</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Created at" xml:space="preserve">
<source>Created at</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Created at: %@" xml:space="preserve">
<source>Created at: %@</source>
<note>copied message info</note>
</trans-unit>
<trans-unit id="Created on %@" xml:space="preserve">
<source>Created on %@</source>
<target>สร้างเมื่อ %@</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Creating link…" xml:space="preserve">
<source>Creating link…</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Current Passcode" xml:space="preserve">
<source>Current Passcode</source>
<target>รหัสผ่านปัจจุบัน</target>
@@ -1862,6 +1883,10 @@ This cannot be undone!</source>
<target>ทำในภายหลัง</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Do not send history to new members." xml:space="preserve">
<source>Do not send history to new members.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Don't create address" xml:space="preserve">
<source>Don't create address</source>
<target>อย่าสร้างที่อยู่</target>
@@ -1932,6 +1957,10 @@ This cannot be undone!</source>
<target>เปิดใช้งานการลบข้อความอัตโนมัติ?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Enable camera access" xml:space="preserve">
<source>Enable camera access</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Enable for all" xml:space="preserve">
<source>Enable for all</source>
<target>เปิดใช้งานสําหรับทุกคน</target>
@@ -1995,6 +2024,10 @@ This cannot be undone!</source>
<target>ข้อความที่ encrypt หรือเหตุการณ์อื่น</target>
<note>notification</note>
</trans-unit>
<trans-unit id="Encrypted message: app is stopped" xml:space="preserve">
<source>Encrypted message: app is stopped</source>
<note>notification</note>
</trans-unit>
<trans-unit id="Encrypted message: database error" xml:space="preserve">
<source>Encrypted message: database error</source>
<target>ข้อความที่ encrypt: ความผิดพลาดในฐานข้อมูล</target>
@@ -2134,6 +2167,10 @@ This cannot be undone!</source>
<source>Error creating member contact</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error creating message" xml:space="preserve">
<source>Error creating message</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error creating profile!" xml:space="preserve">
<source>Error creating profile!</source>
<target>เกิดข้อผิดพลาดในการสร้างโปรไฟล์!</target>
@@ -2218,6 +2255,10 @@ This cannot be undone!</source>
<target>โหลดเซิร์ฟเวอร์ %@ ผิดพลาด</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error opening chat" xml:space="preserve">
<source>Error opening chat</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error receiving file" xml:space="preserve">
<source>Error receiving file</source>
<target>เกิดข้อผิดพลาดในการรับไฟล์</target>
@@ -2258,6 +2299,10 @@ This cannot be undone!</source>
<target>เกิดข้อผิดพลาดในการบันทึกรหัสผ่านผู้ใช้</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error scanning code: %@" xml:space="preserve">
<source>Error scanning code: %@</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error sending email" xml:space="preserve">
<source>Error sending email</source>
<target>เกิดข้อผิดพลาดในการส่งอีเมล</target>
@@ -2581,8 +2626,8 @@ This cannot be undone!</source>
<target>สมาชิกกลุ่มสามารถเพิ่มการแสดงปฏิกิริยาต่อข้อความได้</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Group members can irreversibly delete sent messages." xml:space="preserve">
<source>Group members can irreversibly delete sent messages.</source>
<trans-unit id="Group members can irreversibly delete sent messages. (24 hours)" xml:space="preserve">
<source>Group members can irreversibly delete sent messages. (24 hours)</source>
<target>สมาชิกกลุ่มสามารถลบข้อความที่ส่งแล้วอย่างถาวร</target>
<note>No comment provided by engineer.</note>
</trans-unit>
@@ -2691,6 +2736,10 @@ This cannot be undone!</source>
<target>ประวัติ</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="History is not sent to new members." xml:space="preserve">
<source>History is not sent to new members.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="How SimpleX works" xml:space="preserve">
<source>How SimpleX works</source>
<target>วิธีการ SimpleX ทํางานอย่างไร</target>
@@ -2726,11 +2775,6 @@ This cannot be undone!</source>
<target>หากคุณไม่สามารถพบกันในชีวิตจริงได้ ให้แสดงคิวอาร์โค้ดในวิดีโอคอล หรือแชร์ลิงก์</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="If you cannot meet in person, you can **scan QR code in the video call**, or your contact can share an invitation link." xml:space="preserve">
<source>If you cannot meet in person, you can **scan QR code in the video call**, or your contact can share an invitation link.</source>
<target>หากคุณไม่สามารถพบปะด้วยตนเอง คุณสามารถ **สแกนคิวอาร์โค้ดผ่านการสนทนาทางวิดีโอ** หรือผู้ติดต่อของคุณสามารถแชร์ลิงก์เชิญได้</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="If you enter this passcode when opening the app, all app data will be irreversibly removed!" xml:space="preserve">
<source>If you enter this passcode when opening the app, all app data will be irreversibly removed!</source>
<target>หากคุณใส่รหัสผ่านนี้เมื่อเปิดแอป ข้อมูลแอปทั้งหมดจะถูกลบอย่างถาวร!</target>
@@ -2786,6 +2830,10 @@ This cannot be undone!</source>
<target>นำเข้าฐานข้อมูล</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Improved message delivery" xml:space="preserve">
<source>Improved message delivery</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Improved privacy and security" xml:space="preserve">
<source>Improved privacy and security</source>
<target>ปรับปรุงความเป็นส่วนตัวและความปลอดภัยแล้ว</target>
@@ -2885,15 +2933,31 @@ This cannot be undone!</source>
<target>อินเตอร์เฟซ</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Invalid QR code" xml:space="preserve">
<source>Invalid QR code</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Invalid connection link" xml:space="preserve">
<source>Invalid connection link</source>
<target>ลิงค์เชื่อมต่อไม่ถูกต้อง</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Invalid display name!" xml:space="preserve">
<source>Invalid display name!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Invalid link" xml:space="preserve">
<source>Invalid link</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Invalid name!" xml:space="preserve">
<source>Invalid name!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Invalid response" xml:space="preserve">
<source>Invalid response</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Invalid server address!" xml:space="preserve">
<source>Invalid server address!</source>
<target>ที่อยู่เซิร์ฟเวอร์ไม่ถูกต้อง!</target>
@@ -2984,6 +3048,10 @@ This cannot be undone!</source>
<target>เข้าร่วมกลุ่ม</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Join group conversations" xml:space="preserve">
<source>Join group conversations</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Join group?" xml:space="preserve">
<source>Join group?</source>
<note>No comment provided by engineer.</note>
@@ -3007,10 +3075,18 @@ This is your link for group %@!</source>
<target>กำลังจะเข้าร่วมกลุ่ม</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Keep" xml:space="preserve">
<source>Keep</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Keep the app open to use it from desktop" xml:space="preserve">
<source>Keep the app open to use it from desktop</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Keep unused invitation?" xml:space="preserve">
<source>Keep unused invitation?</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Keep your connections" xml:space="preserve">
<source>Keep your connections</source>
<target>รักษาการเชื่อมต่อของคุณ</target>
@@ -3093,6 +3169,11 @@ This is your link for group %@!</source>
<target>ข้อความสด</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Local" xml:space="preserve">
<source>Local</source>
<target>ในเครื่อง</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Local name" xml:space="preserve">
<source>Local name</source>
<target>ชื่อภายในเครื่องเท่านั้น</target>
@@ -3331,6 +3412,10 @@ This is your link for group %@!</source>
<target>รหัสผ่านใหม่</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="New chat" xml:space="preserve">
<source>New chat</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="New contact request" xml:space="preserve">
<source>New contact request</source>
<target>คำขอติดต่อใหม่</target>
@@ -3452,16 +3537,15 @@ This is your link for group %@!</source>
- ปิดการใช้งานสมาชิก (บทบาท "ผู้สังเกตการณ์")</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="OK" xml:space="preserve">
<source>OK</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Off" xml:space="preserve">
<source>Off</source>
<target>ปิด</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Off (Local)" xml:space="preserve">
<source>Off (Local)</source>
<target>ปิด (ในเครื่อง)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Ok" xml:space="preserve">
<source>Ok</source>
<target>ตกลง</target>
@@ -3522,8 +3606,8 @@ This is your link for group %@!</source>
<target>มีเพียงคุณเท่านั้นที่สามารถแสดงปฏิกิริยาต่อข้อความได้</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Only you can irreversibly delete messages (your contact can mark them for deletion)." xml:space="preserve">
<source>Only you can irreversibly delete messages (your contact can mark them for deletion).</source>
<trans-unit id="Only you can irreversibly delete messages (your contact can mark them for deletion). (24 hours)" xml:space="preserve">
<source>Only you can irreversibly delete messages (your contact can mark them for deletion). (24 hours)</source>
<target>มีเพียงคุณเท่านั้นที่สามารถลบข้อความแบบย้อนกลับไม่ได้ (ผู้ติดต่อของคุณสามารถทำเครื่องหมายเพื่อลบได้)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
@@ -3547,8 +3631,8 @@ This is your link for group %@!</source>
<target>เฉพาะผู้ติดต่อของคุณเท่านั้นที่สามารถเพิ่มการโต้ตอบข้อความได้</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Only your contact can irreversibly delete messages (you can mark them for deletion)." xml:space="preserve">
<source>Only your contact can irreversibly delete messages (you can mark them for deletion).</source>
<trans-unit id="Only your contact can irreversibly delete messages (you can mark them for deletion). (24 hours)" xml:space="preserve">
<source>Only your contact can irreversibly delete messages (you can mark them for deletion). (24 hours)</source>
<target>เฉพาะผู้ติดต่อของคุณเท่านั้นที่สามารถลบข้อความแบบย้อนกลับไม่ได้ (คุณสามารถทำเครื่องหมายเพื่อลบได้)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
@@ -3600,9 +3684,16 @@ This is your link for group %@!</source>
<target>โปรโตคอลและโค้ดโอเพ่นซอร์ส ใคร ๆ ก็สามารถเปิดใช้เซิร์ฟเวอร์ได้</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Opening database…" xml:space="preserve">
<source>Opening database…</source>
<target>กำลังเปิดฐานข้อมูล…</target>
<trans-unit id="Opening app…" xml:space="preserve">
<source>Opening app…</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Or scan QR code" xml:space="preserve">
<source>Or scan QR code</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Or show this code" xml:space="preserve">
<source>Or show this code</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="PING count" xml:space="preserve">
@@ -3645,10 +3736,9 @@ This is your link for group %@!</source>
<target>รหัสผ่านที่จะแสดง</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Paste" xml:space="preserve">
<source>Paste</source>
<target>แปะ</target>
<note>No comment provided by engineer.</note>
<trans-unit id="Past member %@" xml:space="preserve">
<source>Past member %@</source>
<note>past/unknown group member</note>
</trans-unit>
<trans-unit id="Paste desktop address" xml:space="preserve">
<source>Paste desktop address</source>
@@ -3659,14 +3749,13 @@ This is your link for group %@!</source>
<target>แปะภาพ</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Paste received link" xml:space="preserve">
<source>Paste received link</source>
<target>แปะลิงก์ที่ได้รับ</target>
<trans-unit id="Paste link to connect!" xml:space="preserve">
<source>Paste link to connect!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Paste the link you received to connect with your contact." xml:space="preserve">
<source>Paste the link you received to connect with your contact.</source>
<note>placeholder</note>
<trans-unit id="Paste the link you received" xml:space="preserve">
<source>Paste the link you received</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="People can connect to you only via the links you share." xml:space="preserve">
<source>People can connect to you only via the links you share.</source>
@@ -3703,6 +3792,11 @@ This is your link for group %@!</source>
<target>โปรดตรวจสอบความต้องการของคุณและการตั้งค่าผู้ติดต่อ</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Please contact developers.&#10;Error: %@" xml:space="preserve">
<source>Please contact developers.
Error: %@</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Please contact group admin." xml:space="preserve">
<source>Please contact group admin.</source>
<target>โปรดติดต่อผู้ดูแลกลุ่ม</target>
@@ -3788,6 +3882,10 @@ This is your link for group %@!</source>
<target>ชื่อไฟล์ส่วนตัว</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Private notes" xml:space="preserve">
<source>Private notes</source>
<note>name of notes to self</note>
</trans-unit>
<trans-unit id="Profile and server connections" xml:space="preserve">
<source>Profile and server connections</source>
<target>การเชื่อมต่อโปรไฟล์และเซิร์ฟเวอร์</target>
@@ -3906,6 +4004,10 @@ This is your link for group %@!</source>
<target>อ่านเพิ่มเติมใน[คู่มือผู้ใช้](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)." xml:space="preserve">
<source>Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode).</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends)." xml:space="preserve">
<source>Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends).</source>
<target>อ่านเพิ่มเติมใน[คู่มือผู้ใช้](https://simplex.chat/docs/guide/readme.html#connect-to-friends)</target>
@@ -3960,6 +4062,10 @@ This is your link for group %@!</source>
<target>กำลังรับผ่าน</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Recent history and improved [directory bot](simplex:/contact#/?v=1-4&amp;smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion)." xml:space="preserve">
<source>Recent history and improved [directory bot](simplex:/contact#/?v=1-4&amp;smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion).</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Recipients see updates as you type them." xml:space="preserve">
<source>Recipients see updates as you type them.</source>
<target>ผู้รับจะเห็นการอัปเดตเมื่อคุณพิมพ์</target>
@@ -4112,6 +4218,10 @@ This is your link for group %@!</source>
<target>กู้คืนข้อผิดพลาดของฐานข้อมูล</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Retry" xml:space="preserve">
<source>Retry</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Reveal" xml:space="preserve">
<source>Reveal</source>
<target>เปิดเผย</target>
@@ -4237,6 +4347,10 @@ This is your link for group %@!</source>
<target>เซิร์ฟเวอร์ WebRTC ICE ที่บันทึกไว้จะถูกลบออก</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Saved message" xml:space="preserve">
<source>Saved message</source>
<note>message info title</note>
</trans-unit>
<trans-unit id="Scan QR code" xml:space="preserve">
<source>Scan QR code</source>
<target>สแกนคิวอาร์โค้ด</target>
@@ -4266,6 +4380,14 @@ This is your link for group %@!</source>
<target>ค้นหา</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Search bar accepts invitation links." xml:space="preserve">
<source>Search bar accepts invitation links.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Search or paste SimpleX link" xml:space="preserve">
<source>Search or paste SimpleX link</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Secure queue" xml:space="preserve">
<source>Secure queue</source>
<target>คิวที่ปลอดภัย</target>
@@ -4370,6 +4492,10 @@ This is your link for group %@!</source>
<target>ส่งจากแกลเลอรีหรือแป้นพิมพ์แบบกำหนดเอง</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Send up to 100 last messages to new members." xml:space="preserve">
<source>Send up to 100 last messages to new members.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Sender cancelled file transfer." xml:space="preserve">
<source>Sender cancelled file transfer.</source>
<target>ผู้ส่งยกเลิกการโอนไฟล์</target>
@@ -4537,9 +4663,8 @@ This is your link for group %@!</source>
<target>แชร์ลิงก์</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Share one-time invitation link" xml:space="preserve">
<source>Share one-time invitation link</source>
<target>แชร์ลิงก์เชิญแบบใช้ครั้งเดียว</target>
<trans-unit id="Share this 1-time invite link" xml:space="preserve">
<source>Share this 1-time invite link</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Share with contacts" xml:space="preserve">
@@ -4659,16 +4784,15 @@ This is your link for group %@!</source>
<target>ใครบางคน</target>
<note>notification title</note>
</trans-unit>
<trans-unit id="Start a new chat" xml:space="preserve">
<source>Start a new chat</source>
<target>เริ่มแชทใหม่</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Start chat" xml:space="preserve">
<source>Start chat</source>
<target>เริ่มแชท</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Start chat?" xml:space="preserve">
<source>Start chat?</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Start migration" xml:space="preserve">
<source>Start migration</source>
<target>เริ่มการย้ายข้อมูล</target>
@@ -4793,6 +4917,14 @@ This is your link for group %@!</source>
<target>แตะเพื่อเข้าร่วมโหมดไม่ระบุตัวตน</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Tap to paste link" xml:space="preserve">
<source>Tap to paste link</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Tap to scan" xml:space="preserve">
<source>Tap to scan</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Tap to start a new chat" xml:space="preserve">
<source>Tap to start a new chat</source>
<target>แตะเพื่อเริ่มแชทใหม่</target>
@@ -4856,6 +4988,10 @@ It can happen because of some bug or when the connection is compromised.</source
<target>ความพยายามในการเปลี่ยนรหัสผ่านของฐานข้อมูลไม่เสร็จสมบูรณ์</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="The code you scanned is not a SimpleX link QR code." xml:space="preserve">
<source>The code you scanned is not a SimpleX link QR code.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="The connection you accepted will be cancelled!" xml:space="preserve">
<source>The connection you accepted will be cancelled!</source>
<target>การเชื่อมต่อที่คุณยอมรับจะถูกยกเลิก!</target>
@@ -4921,21 +5057,15 @@ It can happen because of some bug or when the connection is compromised.</source
<target>เซิร์ฟเวอร์สำหรับการเชื่อมต่อใหม่ของโปรไฟล์การแชทปัจจุบันของคุณ **%@**</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="The text you pasted is not a SimpleX link." xml:space="preserve">
<source>The text you pasted is not a SimpleX link.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Theme" xml:space="preserve">
<source>Theme</source>
<target>ธีม</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="There should be at least one user profile." xml:space="preserve">
<source>There should be at least one user profile.</source>
<target>ควรมีโปรไฟล์ผู้ใช้อย่างน้อยหนึ่งโปรไฟล์</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="There should be at least one visible user profile." xml:space="preserve">
<source>There should be at least one visible user profile.</source>
<target>ควรมีอย่างน้อยหนึ่งโปรไฟล์ผู้ใช้ที่มองเห็นได้</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="These settings are for your current profile **%@**." xml:space="preserve">
<source>These settings are for your current profile **%@**.</source>
<target>การตั้งค่าเหล่านี้ใช้สำหรับโปรไฟล์ปัจจุบันของคุณ **%@**</target>
@@ -4964,6 +5094,10 @@ It can happen because of some bug or when the connection is compromised.</source
<source>This device name</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="This display name is invalid. Please choose another name." xml:space="preserve">
<source>This display name is invalid. Please choose another name.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="This group has over %lld members, delivery receipts are not sent." xml:space="preserve">
<source>This group has over %lld members, delivery receipts are not sent.</source>
<note>No comment provided by engineer.</note>
@@ -5061,16 +5195,15 @@ You will be prompted to complete authentication before this feature is enabled.<
<target>พยายามเชื่อมต่อกับเซิร์ฟเวอร์ที่ใช้รับข้อความจากผู้ติดต่อนี้</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Turkish interface" xml:space="preserve">
<source>Turkish interface</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Turn off" xml:space="preserve">
<source>Turn off</source>
<target>ปิด</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Turn off notifications?" xml:space="preserve">
<source>Turn off notifications?</source>
<target>ปิดการแจ้งเตือนไหม?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Turn on" xml:space="preserve">
<source>Turn on</source>
<target>เปิด</target>
@@ -5085,10 +5218,18 @@ You will be prompted to complete authentication before this feature is enabled.<
<source>Unblock</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Unblock for all" xml:space="preserve">
<source>Unblock for all</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Unblock member" xml:space="preserve">
<source>Unblock member</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Unblock member for all?" xml:space="preserve">
<source>Unblock member for all?</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Unblock member?" xml:space="preserve">
<source>Unblock member?</source>
<note>No comment provided by engineer.</note>
@@ -5183,6 +5324,10 @@ To connect, please ask your contact to create another connection link and check
<target>เปลี่ยนเป็นยังไม่ได้อ่าน</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Up to 100 last messages are sent to new members." xml:space="preserve">
<source>Up to 100 last messages are sent to new members.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Update" xml:space="preserve">
<source>Update</source>
<target>อัปเดต</target>
@@ -5265,6 +5410,10 @@ To connect, please ask your contact to create another connection link and check
<source>Use new incognito profile</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Use only local notifications?" xml:space="preserve">
<source>Use only local notifications?</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Use server" xml:space="preserve">
<source>Use server</source>
<target>ใช้เซิร์ฟเวอร์</target>
@@ -5341,6 +5490,10 @@ To connect, please ask your contact to create another connection link and check
<target>ดูรหัสความปลอดภัย</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Visible history" xml:space="preserve">
<source>Visible history</source>
<note>chat feature</note>
</trans-unit>
<trans-unit id="Voice messages" xml:space="preserve">
<source>Voice messages</source>
<target>ข้อความเสียง</target>
@@ -5425,11 +5578,19 @@ To connect, please ask your contact to create another connection link and check
<target>เมื่อคุณแชร์โปรไฟล์ที่ไม่ระบุตัวตนกับใครสักคน โปรไฟล์นี้จะใช้สำหรับกลุ่มที่พวกเขาเชิญคุณ</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="With encrypted files and media." xml:space="preserve">
<source>With encrypted files and media.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="With optional welcome message." xml:space="preserve">
<source>With optional welcome message.</source>
<target>พร้อมข้อความต้อนรับที่ไม่บังคับ</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="With reduced battery usage." xml:space="preserve">
<source>With reduced battery usage.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Wrong database passphrase" xml:space="preserve">
<source>Wrong database passphrase</source>
<target>รหัสผ่านฐานข้อมูลไม่ถูกต้อง</target>
@@ -5514,11 +5675,6 @@ Repeat join request?</source>
<target>คุณสามารถรับสายจากหน้าจอล็อกโดยไม่ต้องมีการตรวจสอบสิทธิ์อุปกรณ์และแอป</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button." xml:space="preserve">
<source>You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button.</source>
<target>คุณสามารถเชื่อมต่อได้โดยคลิกที่ลิงค์ หากเปิดในเบราว์เซอร์ ให้คลิกปุ่ม **เปิดในแอปมือถือ**</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You can create it later" xml:space="preserve">
<source>You can create it later</source>
<target>คุณสามารถสร้างได้ในภายหลัง</target>
@@ -5539,6 +5695,10 @@ Repeat join request?</source>
<target>คุณสามารถซ่อนหรือปิดเสียงโปรไฟล์ผู้ใช้ - ปัดไปทางขวา</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You can make it visible to your SimpleX contacts via Settings." xml:space="preserve">
<source>You can make it visible to your SimpleX contacts via Settings.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You can now send messages to %@" xml:space="preserve">
<source>You can now send messages to %@</source>
<target>ตอนนี้คุณสามารถส่งข้อความถึง %@</target>
@@ -5579,6 +5739,10 @@ Repeat join request?</source>
<target>คุณสามารถใช้มาร์กดาวน์เพื่อจัดรูปแบบข้อความ:</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You can view invitation link again in connection details." xml:space="preserve">
<source>You can view invitation link again in connection details.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You can't send messages!" xml:space="preserve">
<source>You can't send messages!</source>
<target>คุณไม่สามารถส่งข้อความได้!</target>
@@ -5762,13 +5926,6 @@ You can cancel this connection and remove the contact (and try later with a new
<target>ผู้ติดต่อของคุณสามารถอนุญาตให้ลบข้อความทั้งหมดได้</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your contacts in SimpleX will see it.&#10;You can change it in Settings." xml:space="preserve">
<source>Your contacts in SimpleX will see it.
You can change it in Settings.</source>
<target>ผู้ติดต่อของคุณใน SimpleX จะเห็น
คุณสามารถเปลี่ยนได้ในการตั้งค่า</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your contacts will remain connected." xml:space="preserve">
<source>Your contacts will remain connected.</source>
<target>ผู้ติดต่อของคุณจะยังคงเชื่อมต่ออยู่</target>
@@ -5916,6 +6073,14 @@ SimpleX servers cannot see your profile.</source>
<source>blocked</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="blocked %@" xml:space="preserve">
<source>blocked %@</source>
<note>rcv group event chat item</note>
</trans-unit>
<trans-unit id="blocked by admin" xml:space="preserve">
<source>blocked by admin</source>
<note>blocked chat item</note>
</trans-unit>
<trans-unit id="bold" xml:space="preserve">
<source>bold</source>
<target>ตัวหนา</target>
@@ -6035,6 +6200,10 @@ SimpleX servers cannot see your profile.</source>
<target>การเชื่อมต่อ:%@</target>
<note>connection information</note>
</trans-unit>
<trans-unit id="contact %@ changed to %@" xml:space="preserve">
<source>contact %1$@ changed to %2$@</source>
<note>profile update event chat item</note>
</trans-unit>
<trans-unit id="contact has e2e encryption" xml:space="preserve">
<source>contact has e2e encryption</source>
<target>ผู้ติดต่อมีการ encrypt จากต้นจนจบ</target>
@@ -6302,6 +6471,10 @@ SimpleX servers cannot see your profile.</source>
<target>สมาชิก</target>
<note>member role</note>
</trans-unit>
<trans-unit id="member %@ changed to %@" xml:space="preserve">
<source>member %1$@ changed to %2$@</source>
<note>profile update event chat item</note>
</trans-unit>
<trans-unit id="member connected" xml:space="preserve">
<source>connected</source>
<target>เชื่อมต่อสำเร็จ</target>
@@ -6424,6 +6597,14 @@ SimpleX servers cannot see your profile.</source>
<target>ถูกลบแล้ว %@</target>
<note>rcv group event chat item</note>
</trans-unit>
<trans-unit id="removed contact address" xml:space="preserve">
<source>removed contact address</source>
<note>profile update event chat item</note>
</trans-unit>
<trans-unit id="removed profile picture" xml:space="preserve">
<source>removed profile picture</source>
<note>profile update event chat item</note>
</trans-unit>
<trans-unit id="removed you" xml:space="preserve">
<source>removed you</source>
<target>ลบคุณออกแล้ว</target>
@@ -6453,6 +6634,14 @@ SimpleX servers cannot see your profile.</source>
<source>send direct message</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="set new contact address" xml:space="preserve">
<source>set new contact address</source>
<note>profile update event chat item</note>
</trans-unit>
<trans-unit id="set new profile picture" xml:space="preserve">
<source>set new profile picture</source>
<note>profile update event chat item</note>
</trans-unit>
<trans-unit id="starting…" xml:space="preserve">
<source>starting…</source>
<target>กำลังเริ่มต้น…</target>
@@ -6468,16 +6657,28 @@ SimpleX servers cannot see your profile.</source>
<target>ผู้ติดต่อนี้</target>
<note>notification title</note>
</trans-unit>
<trans-unit id="unblocked %@" xml:space="preserve">
<source>unblocked %@</source>
<note>rcv group event chat item</note>
</trans-unit>
<trans-unit id="unknown" xml:space="preserve">
<source>unknown</source>
<target>ไม่ทราบ</target>
<note>connection info</note>
</trans-unit>
<trans-unit id="unknown status" xml:space="preserve">
<source>unknown status</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="updated group profile" xml:space="preserve">
<source>updated group profile</source>
<target>อัปเดตโปรไฟล์กลุ่มแล้ว</target>
<note>rcv group event chat item</note>
</trans-unit>
<trans-unit id="updated profile" xml:space="preserve">
<source>updated profile</source>
<note>profile update event chat item</note>
</trans-unit>
<trans-unit id="v%@" xml:space="preserve">
<source>v%@</source>
<note>No comment provided by engineer.</note>
@@ -6547,6 +6748,10 @@ SimpleX servers cannot see your profile.</source>
<target>คุณเป็นผู้สังเกตการณ์</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="you blocked %@" xml:space="preserve">
<source>you blocked %@</source>
<note>snd group event chat item</note>
</trans-unit>
<trans-unit id="you changed address" xml:space="preserve">
<source>you changed address</source>
<target>คุณเปลี่ยนที่อยู่แล้ว</target>
@@ -6587,6 +6792,10 @@ SimpleX servers cannot see your profile.</source>
<target>คุณแชร์ลิงก์แบบใช้ครั้งเดียวโดยไม่ระบุตัวตนแล้ว</target>
<note>chat list item description</note>
</trans-unit>
<trans-unit id="you unblocked %@" xml:space="preserve">
<source>you unblocked %@</source>
<note>snd group event chat item</note>
</trans-unit>
<trans-unit id="you: " xml:space="preserve">
<source>you: </source>
<target>คุณ: </target>

View File

@@ -0,0 +1,15 @@
{
"colors" : [
{
"idiom" : "universal",
"locale" : "tr"
}
],
"properties" : {
"localizable" : true
},
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View File

@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -0,0 +1,23 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"red" : "0.000",
"alpha" : "1.000",
"blue" : "1.000",
"green" : "0.533"
}
},
"idiom" : "universal"
}
],
"properties" : {
"localizable" : true
},
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View File

@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -0,0 +1,6 @@
/* Bundle display name */
"CFBundleDisplayName" = "SimpleX NSE";
/* Bundle name */
"CFBundleName" = "SimpleX NSE";
/* Copyright (human-readable) */
"NSHumanReadableCopyright" = "Copyright © 2022 SimpleX Chat. All rights reserved.";

View File

@@ -0,0 +1,30 @@
/* No comment provided by engineer. */
"_italic_" = "\\_italic_";
/* No comment provided by engineer. */
"**Add new contact**: to create your one-time QR Code for your contact." = "**Add new contact**: to create your one-time QR Code or link for your contact.";
/* No comment provided by engineer. */
"*bold*" = "\\*bold*";
/* No comment provided by engineer. */
"`a + b`" = "\\`a + b`";
/* No comment provided by engineer. */
"~strike~" = "\\~strike~";
/* call status */
"connecting call" = "connecting call…";
/* No comment provided by engineer. */
"Connecting server…" = "Connecting to server…";
/* No comment provided by engineer. */
"Connecting server… (error: %@)" = "Connecting to server… (error: %@)";
/* rcv group event chat item */
"member connected" = "connected";
/* No comment provided by engineer. */
"No group!" = "Group not found!";

View File

@@ -0,0 +1,12 @@
/* Bundle name */
"CFBundleName" = "SimpleX";
/* Privacy - Camera Usage Description */
"NSCameraUsageDescription" = "SimpleX needs camera access to scan QR codes to connect to other users and for video calls.";
/* Privacy - Face ID Usage Description */
"NSFaceIDUsageDescription" = "SimpleX uses Face ID for local authentication";
/* Privacy - Local Network Usage Description */
"NSLocalNetworkUsageDescription" = "SimpleX uses local network access to allow using user chat profile via desktop app on the same network.";
/* Privacy - Microphone Usage Description */
"NSMicrophoneUsageDescription" = "SimpleX needs microphone access for audio and video calls, and to record voice messages.";
/* Privacy - Photo Library Additions Usage Description */
"NSPhotoLibraryAddUsageDescription" = "SimpleX needs access to Photo Library for saving captured and received media";

View File

@@ -0,0 +1,12 @@
{
"developmentRegion" : "en",
"project" : "SimpleX.xcodeproj",
"targetLocale" : "tr",
"toolInfo" : {
"toolBuildNumber" : "15A240d",
"toolID" : "com.apple.dt.xcode",
"toolName" : "Xcode",
"toolVersion" : "15.0"
},
"version" : "1.0"
}

View File

@@ -212,6 +212,10 @@
<source>%lld messages blocked</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%lld messages blocked by admin" xml:space="preserve">
<source>%lld messages blocked by admin</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%lld messages marked deleted" xml:space="preserve">
<source>%lld messages marked deleted</source>
<note>No comment provided by engineer.</note>
@@ -303,14 +307,17 @@
<target>)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="**Add contact**: to create a new invitation link, or connect via a link you received." xml:space="preserve">
<source>**Add contact**: to create a new invitation link, or connect via a link you received.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="**Add new contact**: to create your one-time QR Code for your contact." xml:space="preserve">
<source>**Add new contact**: to create your one-time QR Code or link for your contact.</source>
<target>**添加新联系人**:为您的联系人创建一次性二维码或者链接。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="**Create link / QR code** for your contact to use." xml:space="preserve">
<source>**Create link / QR code** for your contact to use.</source>
<target>**创建链接 / 二维码** 给您的联系人使用。</target>
<trans-unit id="**Create group**: to create a new group." xml:space="preserve">
<source>**Create group**: to create a new group.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have." xml:space="preserve">
@@ -323,11 +330,6 @@
<target>**最私密**:不使用 SimpleX Chat 通知服务器,在后台定期检查消息(取决于您多经常使用应用程序)。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="**Paste received link** or open it in the browser and tap **Open in mobile app**." xml:space="preserve">
<source>**Paste received link** or open it in the browser and tap **Open in mobile app**.</source>
<target>**粘贴收到的链接**或者在浏览器里打开并且点击**在移动应用程序里打开**。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="**Please note**: you will NOT be able to recover or change passphrase if you lose it." xml:space="preserve">
<source>**Please note**: you will NOT be able to recover or change passphrase if you lose it.</source>
<target>**请注意**:如果您丢失密码,您将无法恢复或者更改密码。</target>
@@ -338,11 +340,6 @@
<target>**推荐**:设备令牌和通知会发送至 SimpleX Chat 通知服务器,但是消息内容、大小或者发送人不会。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="**Scan QR code**: to connect to your contact in person or via video call." xml:space="preserve">
<source>**Scan QR code**: to connect to your contact in person or via video call.</source>
<target>**扫描二维码**:见面或者通过视频通话来连接您的联系人。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="**Warning**: Instant push notifications require passphrase saved in Keychain." xml:space="preserve">
<source>**Warning**: Instant push notifications require passphrase saved in Keychain.</source>
<target>**警告**:及时推送通知需要保存在钥匙串的密码。</target>
@@ -440,11 +437,6 @@
<target>1周</target>
<note>time interval</note>
</trans-unit>
<trans-unit id="1-time link" xml:space="preserve">
<source>1-time link</source>
<target>一次性链接</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="5 minutes" xml:space="preserve">
<source>5 minutes</source>
<target>5分钟</target>
@@ -560,6 +552,10 @@
<target>将地址添加到您的个人资料,以便您的联系人可以与其他人共享。个人资料更新将发送给您的联系人。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Add contact" xml:space="preserve">
<source>Add contact</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Add preset servers" xml:space="preserve">
<source>Add preset servers</source>
<target>添加预设服务器</target>
@@ -630,6 +626,10 @@
<target>所有群组成员将保持连接。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="All messages will be deleted - this cannot be undone!" xml:space="preserve">
<source>All messages will be deleted - this cannot be undone!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you." xml:space="preserve">
<source>All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you.</source>
<target>所有聊天记录和消息将被删除——这一行为无法撤销!只有您的消息会被删除。</target>
@@ -664,8 +664,8 @@
<target>仅当您的联系人允许时才允许限时消息。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Allow irreversible message deletion only if your contact allows it to you." xml:space="preserve">
<source>Allow irreversible message deletion only if your contact allows it to you.</source>
<trans-unit id="Allow irreversible message deletion only if your contact allows it to you. (24 hours)" xml:space="preserve">
<source>Allow irreversible message deletion only if your contact allows it to you. (24 hours)</source>
<target>仅有您的联系人许可后才允许不可撤回消息移除。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
@@ -689,8 +689,8 @@
<target>允许发送限时消息。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Allow to irreversibly delete sent messages." xml:space="preserve">
<source>Allow to irreversibly delete sent messages.</source>
<trans-unit id="Allow to irreversibly delete sent messages. (24 hours)" xml:space="preserve">
<source>Allow to irreversibly delete sent messages. (24 hours)</source>
<target>允许不可撤回地删除已发送消息。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
@@ -724,8 +724,8 @@
<target>允许您的联系人给您打电话。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Allow your contacts to irreversibly delete sent messages." xml:space="preserve">
<source>Allow your contacts to irreversibly delete sent messages.</source>
<trans-unit id="Allow your contacts to irreversibly delete sent messages. (24 hours)" xml:space="preserve">
<source>Allow your contacts to irreversibly delete sent messages. (24 hours)</source>
<target>允许您的联系人不可撤回地删除已发送消息。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
@@ -899,6 +899,10 @@
<source>Block</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Block for all" xml:space="preserve">
<source>Block for all</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Block group members" xml:space="preserve">
<source>Block group members</source>
<note>No comment provided by engineer.</note>
@@ -907,17 +911,25 @@
<source>Block member</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Block member for all?" xml:space="preserve">
<source>Block member for all?</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Block member?" xml:space="preserve">
<source>Block member?</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Blocked by admin" xml:space="preserve">
<source>Blocked by admin</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Both you and your contact can add message reactions." xml:space="preserve">
<source>Both you and your contact can add message reactions.</source>
<target>您和您的联系人都可以添加消息回应。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Both you and your contact can irreversibly delete sent messages." xml:space="preserve">
<source>Both you and your contact can irreversibly delete sent messages.</source>
<trans-unit id="Both you and your contact can irreversibly delete sent messages. (24 hours)" xml:space="preserve">
<source>Both you and your contact can irreversibly delete sent messages. (24 hours)</source>
<target>您和您的联系人都可以不可逆转地删除已发送的消息。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
@@ -956,9 +968,8 @@
<target>通话</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Can't delete user profile!" xml:space="preserve">
<source>Can't delete user profile!</source>
<target>无法删除用户个人资料!</target>
<trans-unit id="Camera not available" xml:space="preserve">
<source>Camera not available</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Can't invite contact!" xml:space="preserve">
@@ -1072,6 +1083,10 @@
<target>聊天已停止</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Chat is stopped. If you already used this database on another device, you should transfer it back before starting chat." xml:space="preserve">
<source>Chat is stopped. If you already used this database on another device, you should transfer it back before starting chat.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Chat preferences" xml:space="preserve">
<source>Chat preferences</source>
<target>聊天偏好设置</target>
@@ -1117,6 +1132,10 @@
<target>清除对话吗?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Clear private notes?" xml:space="preserve">
<source>Clear private notes?</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Clear verification" xml:space="preserve">
<source>Clear verification</source>
<target>清除验证</target>
@@ -1208,11 +1227,6 @@ This is your own one-time link!</source>
<target>通过链接连接</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connect via link / QR code" xml:space="preserve">
<source>Connect via link / QR code</source>
<target>通过群组链接/二维码连接</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connect via one-time link" xml:space="preserve">
<source>Connect via one-time link</source>
<target>通过一次性链接连接</target>
@@ -1380,11 +1394,6 @@ This is your own one-time link!</source>
<target>在[桌面应用程序](https://simplex.chat/downloads/)中创建新的个人资料。 💻</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Create one-time invitation link" xml:space="preserve">
<source>Create one-time invitation link</source>
<target>创建一次性邀请链接</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Create profile" xml:space="preserve">
<source>Create profile</source>
<note>No comment provided by engineer.</note>
@@ -1404,11 +1413,23 @@ This is your own one-time link!</source>
<target>创建您的资料</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Created at" xml:space="preserve">
<source>Created at</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Created at: %@" xml:space="preserve">
<source>Created at: %@</source>
<note>copied message info</note>
</trans-unit>
<trans-unit id="Created on %@" xml:space="preserve">
<source>Created on %@</source>
<target>创建于 %@</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Creating link…" xml:space="preserve">
<source>Creating link…</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Current Passcode" xml:space="preserve">
<source>Current Passcode</source>
<target>当前密码</target>
@@ -1880,6 +1901,10 @@ This cannot be undone!</source>
<target>稍后再做</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Do not send history to new members." xml:space="preserve">
<source>Do not send history to new members.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Don't create address" xml:space="preserve">
<source>Don't create address</source>
<target>不创建地址</target>
@@ -1950,6 +1975,10 @@ This cannot be undone!</source>
<target>启用自动删除消息?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Enable camera access" xml:space="preserve">
<source>Enable camera access</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Enable for all" xml:space="preserve">
<source>Enable for all</source>
<target>全部启用</target>
@@ -2015,6 +2044,10 @@ This cannot be undone!</source>
<target>加密消息或其他事件</target>
<note>notification</note>
</trans-unit>
<trans-unit id="Encrypted message: app is stopped" xml:space="preserve">
<source>Encrypted message: app is stopped</source>
<note>notification</note>
</trans-unit>
<trans-unit id="Encrypted message: database error" xml:space="preserve">
<source>Encrypted message: database error</source>
<target>加密消息:数据库错误</target>
@@ -2155,6 +2188,10 @@ This cannot be undone!</source>
<target>创建成员联系人时出错</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error creating message" xml:space="preserve">
<source>Error creating message</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error creating profile!" xml:space="preserve">
<source>Error creating profile!</source>
<target>创建资料错误!</target>
@@ -2240,6 +2277,10 @@ This cannot be undone!</source>
<target>加载 %@ 服务器错误</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error opening chat" xml:space="preserve">
<source>Error opening chat</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error receiving file" xml:space="preserve">
<source>Error receiving file</source>
<target>接收文件错误</target>
@@ -2280,6 +2321,10 @@ This cannot be undone!</source>
<target>保存用户密码时出错</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error scanning code: %@" xml:space="preserve">
<source>Error scanning code: %@</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error sending email" xml:space="preserve">
<source>Error sending email</source>
<target>发送电邮错误</target>
@@ -2604,8 +2649,8 @@ This cannot be undone!</source>
<target>群组成员可以添加信息回应。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Group members can irreversibly delete sent messages." xml:space="preserve">
<source>Group members can irreversibly delete sent messages.</source>
<trans-unit id="Group members can irreversibly delete sent messages. (24 hours)" xml:space="preserve">
<source>Group members can irreversibly delete sent messages. (24 hours)</source>
<target>群组成员可以不可撤回地删除已发送的消息。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
@@ -2714,6 +2759,10 @@ This cannot be undone!</source>
<target>历史记录</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="History is not sent to new members." xml:space="preserve">
<source>History is not sent to new members.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="How SimpleX works" xml:space="preserve">
<source>How SimpleX works</source>
<target>SimpleX的工作原理</target>
@@ -2749,11 +2798,6 @@ This cannot be undone!</source>
<target>如果您不能亲自见面,可以在视频通话中展示二维码,或分享链接。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="If you cannot meet in person, you can **scan QR code in the video call**, or your contact can share an invitation link." xml:space="preserve">
<source>If you cannot meet in person, you can **scan QR code in the video call**, or your contact can share an invitation link.</source>
<target>如果您不能亲自见面,您可以**扫描视频通话中的二维码**,或者您的联系人可以分享邀请链接。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="If you enter this passcode when opening the app, all app data will be irreversibly removed!" xml:space="preserve">
<source>If you enter this passcode when opening the app, all app data will be irreversibly removed!</source>
<target>如果您在打开应用时输入该密码,所有应用程序数据将被不可撤回地删除!</target>
@@ -2809,6 +2853,10 @@ This cannot be undone!</source>
<target>导入数据库</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Improved message delivery" xml:space="preserve">
<source>Improved message delivery</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Improved privacy and security" xml:space="preserve">
<source>Improved privacy and security</source>
<target>改进的隐私和安全</target>
@@ -2909,15 +2957,31 @@ This cannot be undone!</source>
<target>界面</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Invalid QR code" xml:space="preserve">
<source>Invalid QR code</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Invalid connection link" xml:space="preserve">
<source>Invalid connection link</source>
<target>无效的连接链接</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Invalid display name!" xml:space="preserve">
<source>Invalid display name!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Invalid link" xml:space="preserve">
<source>Invalid link</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Invalid name!" xml:space="preserve">
<source>Invalid name!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Invalid response" xml:space="preserve">
<source>Invalid response</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Invalid server address!" xml:space="preserve">
<source>Invalid server address!</source>
<target>无效的服务器地址!</target>
@@ -3009,6 +3073,10 @@ This cannot be undone!</source>
<target>加入群组</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Join group conversations" xml:space="preserve">
<source>Join group conversations</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Join group?" xml:space="preserve">
<source>Join group?</source>
<note>No comment provided by engineer.</note>
@@ -3032,10 +3100,18 @@ This is your link for group %@!</source>
<target>加入群组中</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Keep" xml:space="preserve">
<source>Keep</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Keep the app open to use it from desktop" xml:space="preserve">
<source>Keep the app open to use it from desktop</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Keep unused invitation?" xml:space="preserve">
<source>Keep unused invitation?</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Keep your connections" xml:space="preserve">
<source>Keep your connections</source>
<target>保持连接</target>
@@ -3118,6 +3194,11 @@ This is your link for group %@!</source>
<target>实时消息</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Local" xml:space="preserve">
<source>Local</source>
<target>本地</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Local name" xml:space="preserve">
<source>Local name</source>
<target>本地名称</target>
@@ -3357,6 +3438,10 @@ This is your link for group %@!</source>
<target>新密码</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="New chat" xml:space="preserve">
<source>New chat</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="New contact request" xml:space="preserve">
<source>New contact request</source>
<target>新联系人请求</target>
@@ -3480,16 +3565,15 @@ This is your link for group %@!</source>
- 禁用成员(“观察员”角色)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="OK" xml:space="preserve">
<source>OK</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Off" xml:space="preserve">
<source>Off</source>
<target>关闭</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Off (Local)" xml:space="preserve">
<source>Off (Local)</source>
<target>关闭(本地)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Ok" xml:space="preserve">
<source>Ok</source>
<target>好的</target>
@@ -3550,8 +3634,8 @@ This is your link for group %@!</source>
<target>只有您可以添加消息回应。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Only you can irreversibly delete messages (your contact can mark them for deletion)." xml:space="preserve">
<source>Only you can irreversibly delete messages (your contact can mark them for deletion).</source>
<trans-unit id="Only you can irreversibly delete messages (your contact can mark them for deletion). (24 hours)" xml:space="preserve">
<source>Only you can irreversibly delete messages (your contact can mark them for deletion). (24 hours)</source>
<target>只有您可以不可撤回地删除消息(您的联系人可以将它们标记为删除)。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
@@ -3575,8 +3659,8 @@ This is your link for group %@!</source>
<target>只有您的联系人可以添加消息回应。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Only your contact can irreversibly delete messages (you can mark them for deletion)." xml:space="preserve">
<source>Only your contact can irreversibly delete messages (you can mark them for deletion).</source>
<trans-unit id="Only your contact can irreversibly delete messages (you can mark them for deletion). (24 hours)" xml:space="preserve">
<source>Only your contact can irreversibly delete messages (you can mark them for deletion). (24 hours)</source>
<target>只有您的联系人才能不可撤回地删除消息(您可以将它们标记为删除)。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
@@ -3629,9 +3713,16 @@ This is your link for group %@!</source>
<target>开源协议和代码——任何人都可以运行服务器。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Opening database…" xml:space="preserve">
<source>Opening database…</source>
<target>打开数据库中……</target>
<trans-unit id="Opening app…" xml:space="preserve">
<source>Opening app…</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Or scan QR code" xml:space="preserve">
<source>Or scan QR code</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Or show this code" xml:space="preserve">
<source>Or show this code</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="PING count" xml:space="preserve">
@@ -3674,10 +3765,9 @@ This is your link for group %@!</source>
<target>显示密码</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Paste" xml:space="preserve">
<source>Paste</source>
<target>粘贴</target>
<note>No comment provided by engineer.</note>
<trans-unit id="Past member %@" xml:space="preserve">
<source>Past member %@</source>
<note>past/unknown group member</note>
</trans-unit>
<trans-unit id="Paste desktop address" xml:space="preserve">
<source>Paste desktop address</source>
@@ -3688,15 +3778,13 @@ This is your link for group %@!</source>
<target>粘贴图片</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Paste received link" xml:space="preserve">
<source>Paste received link</source>
<target>粘贴收到的链接</target>
<trans-unit id="Paste link to connect!" xml:space="preserve">
<source>Paste link to connect!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Paste the link you received to connect with your contact." xml:space="preserve">
<source>Paste the link you received to connect with your contact.</source>
<target>将您收到的链接粘贴到下面的框中以与您的联系人联系。</target>
<note>placeholder</note>
<trans-unit id="Paste the link you received" xml:space="preserve">
<source>Paste the link you received</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="People can connect to you only via the links you share." xml:space="preserve">
<source>People can connect to you only via the links you share.</source>
@@ -3733,6 +3821,11 @@ This is your link for group %@!</source>
<target>请检查您和您的联系人偏好设置。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Please contact developers.&#10;Error: %@" xml:space="preserve">
<source>Please contact developers.
Error: %@</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Please contact group admin." xml:space="preserve">
<source>Please contact group admin.</source>
<target>请联系群组管理员。</target>
@@ -3818,6 +3911,10 @@ This is your link for group %@!</source>
<target>私密文件名</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Private notes" xml:space="preserve">
<source>Private notes</source>
<note>name of notes to self</note>
</trans-unit>
<trans-unit id="Profile and server connections" xml:space="preserve">
<source>Profile and server connections</source>
<target>资料和服务器连接</target>
@@ -3936,6 +4033,10 @@ This is your link for group %@!</source>
<target>在 [用户指南](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address) 中阅读更多内容。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)." xml:space="preserve">
<source>Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode).</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends)." xml:space="preserve">
<source>Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends).</source>
<target>在 [用户指南](https://simplex.chat/docs/guide/readme.html#connect-to-friends) 中阅读更多内容。</target>
@@ -3991,6 +4092,10 @@ This is your link for group %@!</source>
<target>接收通过</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Recent history and improved [directory bot](simplex:/contact#/?v=1-4&amp;smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion)." xml:space="preserve">
<source>Recent history and improved [directory bot](simplex:/contact#/?v=1-4&amp;smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion).</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Recipients see updates as you type them." xml:space="preserve">
<source>Recipients see updates as you type them.</source>
<target>对方会在您键入时看到更新。</target>
@@ -4144,6 +4249,10 @@ This is your link for group %@!</source>
<target>恢复数据库错误</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Retry" xml:space="preserve">
<source>Retry</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Reveal" xml:space="preserve">
<source>Reveal</source>
<target>揭示</target>
@@ -4269,6 +4378,10 @@ This is your link for group %@!</source>
<target>已保存的WebRTC ICE服务器将被删除</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Saved message" xml:space="preserve">
<source>Saved message</source>
<note>message info title</note>
</trans-unit>
<trans-unit id="Scan QR code" xml:space="preserve">
<source>Scan QR code</source>
<target>扫描二维码</target>
@@ -4298,6 +4411,14 @@ This is your link for group %@!</source>
<target>搜索</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Search bar accepts invitation links." xml:space="preserve">
<source>Search bar accepts invitation links.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Search or paste SimpleX link" xml:space="preserve">
<source>Search or paste SimpleX link</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Secure queue" xml:space="preserve">
<source>Secure queue</source>
<target>保护队列</target>
@@ -4403,6 +4524,10 @@ This is your link for group %@!</source>
<target>发送它们来自图库或自定义键盘。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Send up to 100 last messages to new members." xml:space="preserve">
<source>Send up to 100 last messages to new members.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Sender cancelled file transfer." xml:space="preserve">
<source>Sender cancelled file transfer.</source>
<target>发送人已取消文件传输。</target>
@@ -4572,9 +4697,8 @@ This is your link for group %@!</source>
<target>分享链接</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Share one-time invitation link" xml:space="preserve">
<source>Share one-time invitation link</source>
<target>分享一次性邀请链接</target>
<trans-unit id="Share this 1-time invite link" xml:space="preserve">
<source>Share this 1-time invite link</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Share with contacts" xml:space="preserve">
@@ -4697,16 +4821,15 @@ This is your link for group %@!</source>
<target>某人</target>
<note>notification title</note>
</trans-unit>
<trans-unit id="Start a new chat" xml:space="preserve">
<source>Start a new chat</source>
<target>开始新聊天</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Start chat" xml:space="preserve">
<source>Start chat</source>
<target>开始聊天</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Start chat?" xml:space="preserve">
<source>Start chat?</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Start migration" xml:space="preserve">
<source>Start migration</source>
<target>开始迁移</target>
@@ -4831,6 +4954,14 @@ This is your link for group %@!</source>
<target>点击以加入隐身聊天</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Tap to paste link" xml:space="preserve">
<source>Tap to paste link</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Tap to scan" xml:space="preserve">
<source>Tap to scan</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Tap to start a new chat" xml:space="preserve">
<source>Tap to start a new chat</source>
<target>点击开始一个新聊天</target>
@@ -4893,6 +5024,10 @@ It can happen because of some bug or when the connection is compromised.</source
<target>更改数据库密码的尝试未完成。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="The code you scanned is not a SimpleX link QR code." xml:space="preserve">
<source>The code you scanned is not a SimpleX link QR code.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="The connection you accepted will be cancelled!" xml:space="preserve">
<source>The connection you accepted will be cancelled!</source>
<target>您接受的连接将被取消!</target>
@@ -4958,21 +5093,15 @@ It can happen because of some bug or when the connection is compromised.</source
<target>您当前聊天资料 **%@** 的新连接服务器。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="The text you pasted is not a SimpleX link." xml:space="preserve">
<source>The text you pasted is not a SimpleX link.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Theme" xml:space="preserve">
<source>Theme</source>
<target>主题</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="There should be at least one user profile." xml:space="preserve">
<source>There should be at least one user profile.</source>
<target>应该至少有一个用户资料。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="There should be at least one visible user profile." xml:space="preserve">
<source>There should be at least one visible user profile.</source>
<target>应该至少有一个可见的用户资料。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="These settings are for your current profile **%@**." xml:space="preserve">
<source>These settings are for your current profile **%@**.</source>
<target>这些设置适用于您当前的配置文件 **%@**。</target>
@@ -5002,6 +5131,10 @@ It can happen because of some bug or when the connection is compromised.</source
<source>This device name</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="This display name is invalid. Please choose another name." xml:space="preserve">
<source>This display name is invalid. Please choose another name.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="This group has over %lld members, delivery receipts are not sent." xml:space="preserve">
<source>This group has over %lld members, delivery receipts are not sent.</source>
<target>该组有超过 %lld 个成员,不发送送货单。</target>
@@ -5101,16 +5234,15 @@ You will be prompted to complete authentication before this feature is enabled.<
<target>正在尝试连接到用于从该联系人接收消息的服务器。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Turkish interface" xml:space="preserve">
<source>Turkish interface</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Turn off" xml:space="preserve">
<source>Turn off</source>
<target>关闭</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Turn off notifications?" xml:space="preserve">
<source>Turn off notifications?</source>
<target>关闭通知?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Turn on" xml:space="preserve">
<source>Turn on</source>
<target>打开</target>
@@ -5125,10 +5257,18 @@ You will be prompted to complete authentication before this feature is enabled.<
<source>Unblock</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Unblock for all" xml:space="preserve">
<source>Unblock for all</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Unblock member" xml:space="preserve">
<source>Unblock member</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Unblock member for all?" xml:space="preserve">
<source>Unblock member for all?</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Unblock member?" xml:space="preserve">
<source>Unblock member?</source>
<note>No comment provided by engineer.</note>
@@ -5223,6 +5363,10 @@ To connect, please ask your contact to create another connection link and check
<target>未读</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Up to 100 last messages are sent to new members." xml:space="preserve">
<source>Up to 100 last messages are sent to new members.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Update" xml:space="preserve">
<source>Update</source>
<target>更新</target>
@@ -5307,6 +5451,10 @@ To connect, please ask your contact to create another connection link and check
<target>使用新的隐身配置文件</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Use only local notifications?" xml:space="preserve">
<source>Use only local notifications?</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Use server" xml:space="preserve">
<source>Use server</source>
<target>使用服务器</target>
@@ -5383,6 +5531,10 @@ To connect, please ask your contact to create another connection link and check
<target>查看安全码</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Visible history" xml:space="preserve">
<source>Visible history</source>
<note>chat feature</note>
</trans-unit>
<trans-unit id="Voice messages" xml:space="preserve">
<source>Voice messages</source>
<target>语音消息</target>
@@ -5467,11 +5619,19 @@ To connect, please ask your contact to create another connection link and check
<target>当您与某人共享隐身聊天资料时,该资料将用于他们邀请您加入的群组。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="With encrypted files and media." xml:space="preserve">
<source>With encrypted files and media.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="With optional welcome message." xml:space="preserve">
<source>With optional welcome message.</source>
<target>带有可选的欢迎消息。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="With reduced battery usage." xml:space="preserve">
<source>With reduced battery usage.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Wrong database passphrase" xml:space="preserve">
<source>Wrong database passphrase</source>
<target>数据库密码错误</target>
@@ -5556,11 +5716,6 @@ Repeat join request?</source>
<target>您可以从锁屏上接听电话,无需设备和应用程序的认证。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button." xml:space="preserve">
<source>You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button.</source>
<target>您也可以通过点击链接进行连接。如果在浏览器中打开,请点击“在移动应用程序中打开”按钮。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You can create it later" xml:space="preserve">
<source>You can create it later</source>
<target>您可以以后创建它</target>
@@ -5581,6 +5736,10 @@ Repeat join request?</source>
<target>您可以隐藏或静音用户个人资料——只需向右滑动。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You can make it visible to your SimpleX contacts via Settings." xml:space="preserve">
<source>You can make it visible to your SimpleX contacts via Settings.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You can now send messages to %@" xml:space="preserve">
<source>You can now send messages to %@</source>
<target>您现在可以给 %@ 发送消息</target>
@@ -5621,6 +5780,10 @@ Repeat join request?</source>
<target>您可以使用 markdown 来编排消息格式:</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You can view invitation link again in connection details." xml:space="preserve">
<source>You can view invitation link again in connection details.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You can't send messages!" xml:space="preserve">
<source>You can't send messages!</source>
<target>您无法发送消息!</target>
@@ -5805,13 +5968,6 @@ You can cancel this connection and remove the contact (and try later with a new
<target>您的联系人可以允许完全删除消息。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your contacts in SimpleX will see it.&#10;You can change it in Settings." xml:space="preserve">
<source>Your contacts in SimpleX will see it.
You can change it in Settings.</source>
<target>您的 SimpleX 的联系人会看到它。
您可以在设置中更改它。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your contacts will remain connected." xml:space="preserve">
<source>Your contacts will remain connected.</source>
<target>与您的联系人保持连接。</target>
@@ -5960,6 +6116,14 @@ SimpleX 服务器无法看到您的资料。</target>
<source>blocked</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="blocked %@" xml:space="preserve">
<source>blocked %@</source>
<note>rcv group event chat item</note>
</trans-unit>
<trans-unit id="blocked by admin" xml:space="preserve">
<source>blocked by admin</source>
<note>blocked chat item</note>
</trans-unit>
<trans-unit id="bold" xml:space="preserve">
<source>bold</source>
<target>加粗</target>
@@ -6080,6 +6244,10 @@ SimpleX 服务器无法看到您的资料。</target>
<target>连接:%@</target>
<note>connection information</note>
</trans-unit>
<trans-unit id="contact %@ changed to %@" xml:space="preserve">
<source>contact %1$@ changed to %2$@</source>
<note>profile update event chat item</note>
</trans-unit>
<trans-unit id="contact has e2e encryption" xml:space="preserve">
<source>contact has e2e encryption</source>
<target>联系人具有端到端加密</target>
@@ -6349,6 +6517,10 @@ SimpleX 服务器无法看到您的资料。</target>
<target>成员</target>
<note>member role</note>
</trans-unit>
<trans-unit id="member %@ changed to %@" xml:space="preserve">
<source>member %1$@ changed to %2$@</source>
<note>profile update event chat item</note>
</trans-unit>
<trans-unit id="member connected" xml:space="preserve">
<source>connected</source>
<target>已连接</target>
@@ -6471,6 +6643,14 @@ SimpleX 服务器无法看到您的资料。</target>
<target>已删除 %@</target>
<note>rcv group event chat item</note>
</trans-unit>
<trans-unit id="removed contact address" xml:space="preserve">
<source>removed contact address</source>
<note>profile update event chat item</note>
</trans-unit>
<trans-unit id="removed profile picture" xml:space="preserve">
<source>removed profile picture</source>
<note>profile update event chat item</note>
</trans-unit>
<trans-unit id="removed you" xml:space="preserve">
<source>removed you</source>
<target>已将您移除</target>
@@ -6501,6 +6681,14 @@ SimpleX 服务器无法看到您的资料。</target>
<target>发送私信</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="set new contact address" xml:space="preserve">
<source>set new contact address</source>
<note>profile update event chat item</note>
</trans-unit>
<trans-unit id="set new profile picture" xml:space="preserve">
<source>set new profile picture</source>
<note>profile update event chat item</note>
</trans-unit>
<trans-unit id="starting…" xml:space="preserve">
<source>starting…</source>
<target>启动中……</target>
@@ -6516,16 +6704,28 @@ SimpleX 服务器无法看到您的资料。</target>
<target>这个联系人</target>
<note>notification title</note>
</trans-unit>
<trans-unit id="unblocked %@" xml:space="preserve">
<source>unblocked %@</source>
<note>rcv group event chat item</note>
</trans-unit>
<trans-unit id="unknown" xml:space="preserve">
<source>unknown</source>
<target>未知</target>
<note>connection info</note>
</trans-unit>
<trans-unit id="unknown status" xml:space="preserve">
<source>unknown status</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="updated group profile" xml:space="preserve">
<source>updated group profile</source>
<target>已更新的群组资料</target>
<note>rcv group event chat item</note>
</trans-unit>
<trans-unit id="updated profile" xml:space="preserve">
<source>updated profile</source>
<note>profile update event chat item</note>
</trans-unit>
<trans-unit id="v%@" xml:space="preserve">
<source>v%@</source>
<note>No comment provided by engineer.</note>
@@ -6595,6 +6795,10 @@ SimpleX 服务器无法看到您的资料。</target>
<target>您是观察者</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="you blocked %@" xml:space="preserve">
<source>you blocked %@</source>
<note>snd group event chat item</note>
</trans-unit>
<trans-unit id="you changed address" xml:space="preserve">
<source>you changed address</source>
<target>您已更改地址</target>
@@ -6635,6 +6839,10 @@ SimpleX 服务器无法看到您的资料。</target>
<target>您分享了一次性链接隐身聊天</target>
<note>chat list item description</note>
</trans-unit>
<trans-unit id="you unblocked %@" xml:space="preserve">
<source>you unblocked %@</source>
<note>snd group event chat item</note>
</trans-unit>
<trans-unit id="you: " xml:space="preserve">
<source>you: </source>
<target>您: </target>

View File

@@ -5868,6 +5868,36 @@ It can happen because of some bug or when the connection is compromised.</source
<target state="translated">你和你的聯絡人可以新增訊息互動。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%@ connected" xml:space="preserve" approved="no">
<source>%@ connected</source>
<target state="translated">%@ 已連接</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="# %@" xml:space="preserve" approved="no">
<source># %@</source>
<target state="translated"># %@</target>
<note>copied message info title, # &lt;title&gt;</note>
</trans-unit>
<trans-unit id="%@ and %@" xml:space="preserve" approved="no">
<source>%@ and %@</source>
<target state="translated">%@ 和 %@</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="## History" xml:space="preserve" approved="no">
<source>## History</source>
<target state="translated">紀錄</target>
<note>copied message info</note>
</trans-unit>
<trans-unit id="## In reply to" xml:space="preserve" approved="no">
<source>## In reply to</source>
<target state="translated">回覆</target>
<note>copied message info</note>
</trans-unit>
<trans-unit id="%@ and %@ connected" xml:space="preserve" approved="no">
<source>%@ and %@ connected</source>
<target state="translated">%@ 和 %@ 已連接</target>
<note>No comment provided by engineer.</note>
</trans-unit>
</body>
</file>
<file original="en.lproj/SimpleX--iOS--InfoPlist.strings" source-language="en" target-language="zh-Hant" datatype="plaintext">

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