Compare commits

...

583 Commits

Author SHA1 Message Date
Evgeny Poberezkin f3e5ec8030 Merge branch 'stable' into stable-android 2024-12-13 09:37:44 +00:00
Evgeny Poberezkin 4d82afe602 6.2.1: ios 255, android 261, desktop 83 2024-12-12 21:00:16 +00:00
Diogo 591f74a8e3 android, desktop: show contact reactions (#5376)
* android, desktop: show contact reactions

* refactor

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-12-12 20:33:50 +00:00
Evgeny aede65db14 ios: show who reacted for contacts (#5375) 2024-12-12 18:57:40 +00:00
Stanislav Dmitrenko 0bf82c08a1 android, desktop: fix importing the same database after exporting it (#5372)
* android, desktop: fix importing the same database after exporting it

* delete archive after export or cancel

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-12-12 17:07:08 +00:00
Stanislav Dmitrenko ddef5a122c android, desktop: disable scroll to top of the message when chat bubble was clicked (#5365) 2024-12-12 16:25:51 +00:00
Stanislav Dmitrenko 0fdd2e04cc android, desktop: hide debug logs by default (#5362)
* android, desktop: hide debug logs by default

* string

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-12-12 16:14:55 +00:00
Stanislav Dmitrenko d1e66386f5 android: fix bottom bar positon in chat item info page (#5373)
* android: fix bottom bar positon in chat item info page

* change
2024-12-12 15:56:59 +00:00
spaced4ndy 708368c9c5 android, desktop: fix operators text (#5371) 2024-12-12 07:26:17 +00:00
Evgeny Poberezkin 5d8e11c3a2 blog: typo in screenshot 2024-12-10 18:39:14 +00:00
Evgeny Poberezkin df8ce2b06d blog: fix image 2024-12-10 18:13:11 +00:00
Evgeny 6ff5f31bee blog: v6.2 announcement (#5359)
* blog: v6.2 announcement

* update, images

* update

* readme

* correction

* update
2024-12-10 17:16:34 +00:00
Stanislav Dmitrenko 5e29cda27b desktop: start using continuous runtime for AppImage again (#5366) 2024-12-10 11:09:01 +00:00
Stanislav Dmitrenko 2e573662d5 android, desktop: hiding counters while scrolling (#5363)
* android, desktop: hiding counters while scrolling

* change
2024-12-10 10:07:49 +00:00
spaced4ndy 4075c26dd2 ui: offer to fix connection on call and message buttons (#5358) 2024-12-09 21:03:56 +04:00
Evgeny Poberezkin d64351b760 ui: update label in server statistics 2024-12-08 15:17:32 +00:00
sh b06211bd4e flatpak: update metainfo (#5353)
* flatpak: update metainfo

* make notes like in release

* simpler

* space

---------

Co-authored-by: Evgeny <evgeny@poberezkin.com>
2024-12-08 10:39:31 +00:00
Evgeny Poberezkin df21d11d33 Merge branch 'stable' into stable-android 2024-12-08 09:30:39 +00:00
Evgeny Poberezkin 33bc539e16 6.2: ios 254, android 259, desktop 82 2024-12-07 20:53:01 +00:00
Evgeny Poberezkin febea096db android, desktop: remove duplicate translation key 2024-12-07 18:53:18 +00:00
spaced4ndy f0781adbd3 desktop: fix opening operators on onboarding (#5351) 2024-12-07 18:51:23 +00:00
Evgeny Poberezkin df30bb99cb ui: add translations 2024-12-07 17:51:53 +00:00
Evgeny Poberezkin 7c86484978 ios: update library 2024-12-07 17:22:14 +00:00
Stanislav Dmitrenko 307211a47f android, desktop: landscape calls on Android and better local camera ratio management (#5124)
* android, desktop: landscape calls on Android and better local camera ratio management

The main thing is that now when exiting from CallActivity while in call
audio devices are not reset to default. It allows to have landscape mode
enabled

* styles

* fix changing calls
2024-12-07 17:09:00 +00:00
Evgeny 7d6c7c58d7 ui: translations (#5338)
* Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (2211 of 2211 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% (2210 of 2210 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% (2210 of 2210 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% (2206 of 2210 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% (1931 of 1931 strings)

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

* Translated using Weblate (Italian)

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

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

* Translated using Weblate (Hungarian)

Currently translated at 100.0% (2210 of 2210 strings)

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

* Translated using Weblate (Arabic)

Currently translated at 97.4% (2153 of 2210 strings)

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

* Translated using Weblate (Ukrainian)

Currently translated at 99.8% (2206 of 2210 strings)

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

* Translated using Weblate (Arabic)

Currently translated at 98.3% (2174 of 2210 strings)

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

* Translated using Weblate (Ukrainian)

Currently translated at 100.0% (2210 of 2210 strings)

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

* Translated using Weblate (Italian)

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

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

* Translated using Weblate (Ukrainian)

Currently translated at 100.0% (2212 of 2212 strings)

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

* Translated using Weblate (Hungarian)

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

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

* Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (2211 of 2211 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% (2210 of 2210 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% (2210 of 2210 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% (2206 of 2210 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% (1931 of 1931 strings)

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

* Translated using Weblate (Italian)

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

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

* Translated using Weblate (Hungarian)

Currently translated at 100.0% (2210 of 2210 strings)

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

* Translated using Weblate (Arabic)

Currently translated at 97.4% (2153 of 2210 strings)

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

* Translated using Weblate (Ukrainian)

Currently translated at 99.8% (2206 of 2210 strings)

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

* Translated using Weblate (Arabic)

Currently translated at 98.3% (2174 of 2210 strings)

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

* Translated using Weblate (Ukrainian)

Currently translated at 100.0% (2210 of 2210 strings)

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

* Translated using Weblate (Italian)

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

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

* Translated using Weblate (Ukrainian)

Currently translated at 100.0% (2212 of 2212 strings)

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

* Translated using Weblate (Hungarian)

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

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

* Translated using Weblate (Arabic)

Currently translated at 100.0% (2212 of 2212 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 99.8% (2208 of 2212 strings)

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

* Translated using Weblate (Arabic)

Currently translated at 100.0% (2212 of 2212 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 100.0% (2212 of 2212 strings)

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

* Translated using Weblate (Arabic)

Currently translated at 100.0% (2212 of 2212 strings)

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

* Translated using Weblate (Arabic)

Currently translated at 100.0% (2212 of 2212 strings)

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

* Translated using Weblate (German)

Currently translated at 97.5% (2158 of 2212 strings)

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

* Translated using Weblate (Hungarian)

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

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

* Translated using Weblate (German)

Currently translated at 98.7% (2185 of 2213 strings)

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

* Translated using Weblate (Ukrainian)

Currently translated at 100.0% (2213 of 2213 strings)

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

* Translated using Weblate (Hungarian)

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

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

* Translated using Weblate (German)

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

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

* Translated using Weblate (Chinese (Simplified Han script))

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

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

* Translated using Weblate (Hungarian)

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

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

* process localizations

---------

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: J R <jr@simplex.chat>
Co-authored-by: Random <random-r@users.noreply.hosted.weblate.org>
Co-authored-by: jonnysemon <jonnysemon@users.noreply.hosted.weblate.org>
Co-authored-by: Bezruchenko Simon <worcposj44@gmail.com>
Co-authored-by: M1K4 <oomikaoo@gmail.com>
Co-authored-by: mlanp <github@lang.xyz>
Co-authored-by: No name <CertainBot@users.noreply.hosted.weblate.org>
2024-12-07 16:52:34 +00:00
Evgeny 93319d947d website: translations (#5350)
* Translated using Weblate (Arabic)

Currently translated at 100.0% (258 of 258 strings)

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

* Translated using Weblate (Ukrainian)

Currently translated at 100.0% (258 of 258 strings)

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

* Translated using Weblate (Arabic)

Currently translated at 100.0% (258 of 258 strings)

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

* Translated using Weblate (Ukrainian)

Currently translated at 100.0% (258 of 258 strings)

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

* Translated using Weblate (German)

Currently translated at 100.0% (258 of 258 strings)

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

---------

Co-authored-by: jonnysemon <jonnysemon@users.noreply.hosted.weblate.org>
Co-authored-by: Bezruchenko Simon <worcposj44@gmail.com>
Co-authored-by: mlanp <github@lang.xyz>
2024-12-07 16:11:30 +00:00
Evgeny Poberezkin ea4927c9b0 core: 6.2.0.7 updated version 2024-12-07 16:01:57 +00:00
Evgeny fe0d811bf7 ui: operator information (#5343)
* ios: operator information

* android, desktop: operator information

* move texts, simplify navigation
2024-12-07 14:41:54 +00:00
Evgeny Poberezkin cbb3da8f83 core: 6.2.0.7 (simplexmq: 6.2.0.7) 2024-12-07 14:40:35 +00:00
Stanislav Dmitrenko 83f0bd9fd3 android, desktop: onboarding button multiline layout (#5348)
Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-12-07 14:24:14 +00:00
Diogo 615c483912 android: onboarding small design adjustments (#5346)
* android: onboarding small design adjustments

* bigger

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-12-07 14:20:01 +00:00
spaced4ndy e0c2272fcb android, desktop: operators info on onboarding (#5341) 2024-12-06 21:35:10 +04:00
spaced4ndy 362581432c ios: export localizations, add translations (#5339)
* ios: export localizations, add translations

* import
2024-12-06 16:01:55 +00:00
Diogo df1a471c56 ios: remove all unsafe warnings in group preferences save (#5340) 2024-12-06 19:55:15 +04:00
Diogo 7d43a43e82 ios: ask for confirmation of save on contact preferences sheet dismiss (#5337) 2024-12-06 18:44:56 +04:00
spaced4ndy ae8ad5c639 ios: operators info on onboarding (#5336) 2024-12-06 17:49:57 +04:00
spaced4ndy 1408d75eb3 ios: use async getServerOperators api (#5334) 2024-12-06 17:21:55 +04:00
spaced4ndy f408988035 ios: fix oneHandUI setting becoming enabled on import (#5335) 2024-12-06 13:05:39 +00:00
spaced4ndy 945c5015d8 ui: improve pending connection texts (#5333)
* ui: improve contact request text

* android

* ternary

* shorter

* kotlin

* change

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-12-06 15:35:26 +04:00
Diogo 2e431c5afa ios: fix some real time updates in group members (#5332)
* ios: fix some real time updates in group members

* use chat instead of binding for group info updates
2024-12-06 11:10:52 +00:00
Diogo 924273191e ios: ask for confirmation of save on group preferences sheet dismiss (#5327)
* ios: ask for confirmation of save on group preferences sheet dismiss

* fix exit without saving temporary state and also apply fix on dismiss during group creation
2024-12-06 10:21:58 +00:00
Evgeny Poberezkin 9b82cc3303 core: fix feature items when updating preferences in business chats 2024-12-06 10:18:48 +00:00
Evgeny Poberezkin 19e2cebd68 android, desktop: remove footer in Run chat section when chat is running 2024-12-06 01:16:42 +00:00
Evgeny Poberezkin 5ef14ca95e 6.2-beta.6: ios 253, android 258, desktop 81 2024-12-05 23:30:05 +00:00
Evgeny Poberezkin 886dc56de8 ui: update business chat info type 2024-12-05 23:01:37 +00:00
Evgeny Poberezkin bd2ca74987 ui: update "server operators privacy" in onboarding 2024-12-05 22:22:03 +00:00
Evgeny 586671c307 website: translations (#5331)
* Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (258 of 258 strings)

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

* Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (258 of 258 strings)

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (258 of 258 strings)

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

* Translated using Weblate (Hungarian)

Currently translated at 100.0% (258 of 258 strings)

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

---------

Co-authored-by: 大王叫我来巡山 <hamburger2048@users.noreply.hosted.weblate.org>
Co-authored-by: Random <random-r@users.noreply.hosted.weblate.org>
Co-authored-by: Ghost of Sparta <makesocialfoss32@keemail.me>
2024-12-05 21:46:35 +00:00
Evgeny ff504702de ui: translations (#5330)
* Translated using Weblate (German)

Currently translated at 97.5% (2155 of 2209 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 93.2% (2060 of 2209 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 97.5% (2155 of 2209 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 97.6% (2157 of 2209 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 97.5% (2154 of 2209 strings)

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

* Translated using Weblate (Portuguese (Brazil))

Currently translated at 92.3% (2041 of 2209 strings)

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

* Translated using Weblate (Arabic)

Currently translated at 97.4% (2153 of 2209 strings)

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

* Translated using Weblate (Ukrainian)

Currently translated at 97.5% (2155 of 2209 strings)

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

* Translated using Weblate (Polish)

Currently translated at 93.2% (2059 of 2209 strings)

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

* Translated using Weblate (Russian)

Currently translated at 93.2% (2061 of 2210 strings)

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

* Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 97.4% (2154 of 2210 strings)

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

* Translated using Weblate (Chinese (Simplified Han script))

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

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

* Translated using Weblate (Italian)

Currently translated at 98.9% (2187 of 2210 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.5% (2201 of 2210 strings)

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

* Translated using Weblate (Russian)

Currently translated at 100.0% (2210 of 2210 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 96.6% (1868 of 1932 strings)

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

* Translated using Weblate (Italian)

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

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

* Translated using Weblate (Indonesian)

Currently translated at 60.0% (1326 of 2210 strings)

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

* Translated using Weblate (Russian)

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

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

* Translated using Weblate (German)

Currently translated at 97.5% (2155 of 2209 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 93.2% (2060 of 2209 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 97.5% (2155 of 2209 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 97.6% (2157 of 2209 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 97.5% (2154 of 2209 strings)

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

* Translated using Weblate (Portuguese (Brazil))

Currently translated at 92.3% (2041 of 2209 strings)

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

* Translated using Weblate (Arabic)

Currently translated at 97.4% (2153 of 2209 strings)

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

* Translated using Weblate (Ukrainian)

Currently translated at 97.5% (2155 of 2209 strings)

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

* Translated using Weblate (Polish)

Currently translated at 93.2% (2059 of 2209 strings)

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

* Translated using Weblate (Russian)

Currently translated at 93.2% (2061 of 2210 strings)

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

* Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 97.4% (2154 of 2210 strings)

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

* Translated using Weblate (Chinese (Simplified Han script))

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

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

* Translated using Weblate (Italian)

Currently translated at 98.9% (2187 of 2210 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.5% (2201 of 2210 strings)

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

* Translated using Weblate (Russian)

Currently translated at 100.0% (2210 of 2210 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 96.6% (1868 of 1932 strings)

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

* Translated using Weblate (Italian)

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

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

* Translated using Weblate (Indonesian)

Currently translated at 60.0% (1326 of 2210 strings)

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

* Translated using Weblate (Russian)

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

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

* process localizations

* update translations

---------

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: 大王叫我来巡山 <hamburger2048@users.noreply.hosted.weblate.org>
Co-authored-by: M1K4 <oomikaoo@gmail.com>
Co-authored-by: summoner001 <summoner@vivaldi.net>
Co-authored-by: Random <random-r@users.noreply.hosted.weblate.org>
Co-authored-by: Rafi <rafimuhmad90@gmail.com>
2024-12-05 21:42:53 +00:00
spaced4ndy 69e23ad58f android, desktop: don't show unwanted notifications (#5328)
* android, desktop: don't show unwanted notifications

* format

* fix

* code style

---------

Co-authored-by: Avently <7953703+avently@users.noreply.github.com>
2024-12-05 18:45:19 +00:00
Evgeny Poberezkin e9bf229a9d core: 6.2.0.6 2024-12-05 18:36:07 +00:00
Evgeny 60e0e454e8 core: only update business chat preferences (#5325)
* core: only update business chat preferences

* rework

* migration

* test, bump protocol version

* fix tests
2024-12-05 18:32:00 +00:00
Stanislav Dmitrenko de76e271a8 android, desktop: displaying deleted message with file in chat list (#5329) 2024-12-05 18:10:36 +00:00
Diogo 5f66c29dbd ios: fix open from notification and connected directly chat item chat loading (#5326)
* ios: fix opening from notification and connected directly chat item chat loading

* better fix
2024-12-05 16:15:24 +00:00
spaced4ndy 97cd2682d7 core: take address lock before reading contact request data (to prevent possible race condition if user quickly accepts request several times in a row); android, desktop: show error context in agent CMD errors (#5324) 2024-12-05 20:10:44 +04:00
Stanislav Dmitrenko 4f8a70a6c1 android, desktop: allow to scan QR multiple times after fail (#5323) 2024-12-05 15:52:45 +00:00
Evgeny Poberezkin c1a0943448 6.2-beta.5: ios 251, android 256, desktop 80 2024-12-04 21:56:09 +00:00
Evgeny Poberezkin c1c17d1f19 core: 6.2.0.5 (simplexmq: 6.2.0.6) 2024-12-04 19:59:42 +00:00
spaced4ndy 009d13210f android: fix simplex chat team contact card opening empty page on connection (#5320) 2024-12-04 17:33:30 +00:00
spaced4ndy 892f6498be ios: fix contact cards opening empty page on connection 2 (#5319) 2024-12-04 17:33:12 +00:00
spaced4ndy 3fa1d7b07c core: fix cli servers override (#5317) 2024-12-04 16:50:19 +00:00
Stanislav Dmitrenko ee146cdc7b android, desktop, core: option to show toolbar in chat at the top in reachable UI (#5316)
* android, desktop: ability to show toolbar in chat at the top in reachable UI

* rename

* core AppSettings

* ios AppSettings

* rename

* strings, enable reachable chat toolbar when enabled reachable app toolbars

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-12-04 16:49:45 +00:00
Stanislav Dmitrenko 219381f941 android, desktop: don't load new messages when pressing quote while searching (#5315) 2024-12-04 16:33:14 +00:00
Evgeny 89f380400e core: update business chat profile (#5313)
* core: update business chat profile

* fix, test

* refactor

* test changing non-title member profile

* fix history
2024-12-04 16:32:01 +00:00
sh 4f1cf6e79f docs: business page, technical advice (#5314)
* docs/business: populate technical advice sections

* dev tools

* update

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-12-04 10:24:31 +00:00
spaced4ndy 6ff7d4a73c core: fix business chat state on accept (fixes icon) (#5312) 2024-12-04 12:12:30 +04:00
Evgeny Poberezkin 3acc69c6d8 6.2-beta.4: ios 250, android 255, desktop 79 2024-12-03 20:13:09 +00:00
Evgeny Poberezkin b9777c92a5 core: 6.2.0.4 (simplexmq: 6.2.0.5) 2024-12-03 18:52:06 +00:00
Evgeny Poberezkin f3be723cde ios: export localizations 2024-12-03 18:40:58 +00:00
Evgeny a182cf5730 ui, site: v6.2 whats new, business (#5309)
* ui, site: v6.2 whats new, business

* icon

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

* business

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

* typo

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

* typo

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

---------

Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
2024-12-03 18:23:24 +00:00
spaced4ndy 247d12fa40 android, desktop: make more texts different for groups and business chats; ios: preferences texts (#5308) 2024-12-03 21:44:06 +04:00
spaced4ndy 6593de89c2 ios: make more texts different for groups and business chats (#5307) 2024-12-03 15:25:15 +00:00
Evgeny a1e25620f7 website: translations (#5306)
* Translated using Weblate (German)

Currently translated at 100.0% (257 of 257 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 100.0% (257 of 257 strings)

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

* Translated using Weblate (Arabic)

Currently translated at 100.0% (257 of 257 strings)

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (257 of 257 strings)

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

* Translated using Weblate (Ukrainian)

Currently translated at 100.0% (257 of 257 strings)

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

* Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (257 of 257 strings)

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

* Translated using Weblate (Hungarian)

Currently translated at 100.0% (257 of 257 strings)

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

* Translated using Weblate (German)

Currently translated at 100.0% (257 of 257 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 100.0% (257 of 257 strings)

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

* Translated using Weblate (Arabic)

Currently translated at 100.0% (257 of 257 strings)

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (257 of 257 strings)

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

* Translated using Weblate (Ukrainian)

Currently translated at 100.0% (257 of 257 strings)

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

* Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (257 of 257 strings)

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

* Translated using Weblate (Hungarian)

Currently translated at 100.0% (257 of 257 strings)

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

---------

Co-authored-by: mlanp <github@lang.xyz>
Co-authored-by: M1K4 <oomikaoo@gmail.com>
Co-authored-by: jonnysemon <jonnysemon@users.noreply.hosted.weblate.org>
Co-authored-by: Random <random-r@users.noreply.hosted.weblate.org>
Co-authored-by: Max <Prototypem95@users.noreply.hosted.weblate.org>
Co-authored-by: 大王叫我来巡山 <hamburger2048@users.noreply.hosted.weblate.org>
Co-authored-by: summoner001 <summoner@vivaldi.net>
2024-12-03 15:23:23 +00:00
Evgeny 29b9abf241 ui: translations (#5267)
* Translated using Weblate (German)

Currently translated at 95.6% (2087 of 2182 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 95.6% (2087 of 2182 strings)

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

* Translated using Weblate (French)

Currently translated at 95.6% (2087 of 2182 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 95.6% (2087 of 2182 strings)

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

* Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 95.6% (2087 of 2182 strings)

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

* Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 80.7% (1763 of 2182 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 95.6% (2087 of 2182 strings)

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

* Translated using Weblate (Japanese)

Currently translated at 88.6% (1935 of 2182 strings)

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

* Translated using Weblate (Czech)

Currently translated at 91.9% (2006 of 2182 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 95.6% (2087 of 2182 strings)

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

* Translated using Weblate (Ukrainian)

Currently translated at 95.5% (2085 of 2182 strings)

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

* Translated using Weblate (Finnish)

Currently translated at 67.5% (1474 of 2182 strings)

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

* Translated using Weblate (Polish)

Currently translated at 95.6% (2086 of 2182 strings)

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

* Translated using Weblate (Portuguese)

Currently translated at 43.9% (959 of 2182 strings)

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

* Translated using Weblate (Hebrew)

Currently translated at 86.2% (1883 of 2182 strings)

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

* Translated using Weblate (Bulgarian)

Currently translated at 79.1% (1726 of 2182 strings)

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

* Translated using Weblate (Turkish)

Currently translated at 95.6% (2087 of 2182 strings)

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

* Translated using Weblate (Persian)

Currently translated at 83.3% (1819 of 2182 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 33.1% (723 of 2182 strings)

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

* Translated using Weblate (Vietnamese)

Currently translated at 61.5% (1342 of 2182 strings)

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

* Translated using Weblate (German)

Currently translated at 95.6% (2087 of 2182 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 95.6% (2087 of 2182 strings)

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

* Translated using Weblate (French)

Currently translated at 95.6% (2087 of 2182 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 95.6% (2087 of 2182 strings)

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

* Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 95.6% (2087 of 2182 strings)

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

* Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 80.7% (1763 of 2182 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 95.6% (2087 of 2182 strings)

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

* Translated using Weblate (Japanese)

Currently translated at 88.6% (1935 of 2182 strings)

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

* Translated using Weblate (Czech)

Currently translated at 91.9% (2006 of 2182 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 95.6% (2087 of 2182 strings)

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

* Translated using Weblate (Ukrainian)

Currently translated at 95.5% (2085 of 2182 strings)

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

* Translated using Weblate (Finnish)

Currently translated at 67.5% (1474 of 2182 strings)

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

* Translated using Weblate (Polish)

Currently translated at 95.6% (2086 of 2182 strings)

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

* Translated using Weblate (Portuguese)

Currently translated at 43.9% (959 of 2182 strings)

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

* Translated using Weblate (Hebrew)

Currently translated at 86.2% (1883 of 2182 strings)

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

* Translated using Weblate (Bulgarian)

Currently translated at 79.1% (1726 of 2182 strings)

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

* Translated using Weblate (Turkish)

Currently translated at 95.6% (2087 of 2182 strings)

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

* Translated using Weblate (Persian)

Currently translated at 83.3% (1819 of 2182 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 33.1% (723 of 2182 strings)

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

* Translated using Weblate (Vietnamese)

Currently translated at 61.5% (1342 of 2182 strings)

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

* Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (2182 of 2182 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 95.9% (2093 of 2182 strings)

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

* Translated using Weblate (Turkish)

Currently translated at 96.1% (2099 of 2182 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% (2182 of 2182 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% (2182 of 2182 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% (1918 of 1918 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% (2182 of 2182 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% (1918 of 1918 strings)

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

* Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (2182 of 2182 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 98.4% (2148 of 2182 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 100.0% (2182 of 2182 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.0% (1900 of 1918 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 100.0% (2182 of 2182 strings)

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

* Translated using Weblate (Ukrainian)

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

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

* Translated using Weblate (Hungarian)

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

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

* Translated using Weblate (Vietnamese)

Currently translated at 62.4% (1362 of 2182 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 99.0% (2161 of 2182 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 100.0% (1918 of 1918 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 100.0% (2182 of 2182 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 99.5% (1910 of 1918 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 99.6% (1911 of 1918 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% (2178 of 2178 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% (1918 of 1918 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% (2178 of 2178 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% (2178 of 2178 strings)

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

* Translated using Weblate (Ukrainian)

Currently translated at 100.0% (2178 of 2178 strings)

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (2178 of 2178 strings)

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

* Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (2178 of 2178 strings)

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

* Translated using Weblate (Korean)

Currently translated at 66.6% (1452 of 2178 strings)

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

* Translated using Weblate (Vietnamese)

Currently translated at 62.7% (1367 of 2178 strings)

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

* Translated using Weblate (Indonesian)

Currently translated at 59.3% (1293 of 2178 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 100.0% (1918 of 1918 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 99.9% (2177 of 2178 strings)

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

* Translated using Weblate (Czech)

Currently translated at 91.9% (2002 of 2178 strings)

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

* Translated using Weblate (Vietnamese)

Currently translated at 62.9% (1372 of 2178 strings)

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

* Translated using Weblate (Vietnamese)

Currently translated at 63.6% (1387 of 2178 strings)

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

* Translated using Weblate (German)

Currently translated at 100.0% (2178 of 2178 strings)

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

* Translated using Weblate (Japanese)

Currently translated at 62.5% (1199 of 1918 strings)

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

* fix/process localizations

---------

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: 大王叫我来巡山 <hamburger2048@users.noreply.hosted.weblate.org>
Co-authored-by: M1K4 <oomikaoo@gmail.com>
Co-authored-by: Abdullah Koyuncu <wisewebworks@outlook.com>
Co-authored-by: summoner001 <summoner@vivaldi.net>
Co-authored-by: mlanp <github@lang.xyz>
Co-authored-by: Random <random-r@users.noreply.hosted.weblate.org>
Co-authored-by: No name <CertainBot@users.noreply.hosted.weblate.org>
Co-authored-by: jonnysemon <jonnysemon@users.noreply.hosted.weblate.org>
Co-authored-by: Max <Prototypem95@users.noreply.hosted.weblate.org>
Co-authored-by: Bezruchenko Simon <worcposj44@gmail.com>
Co-authored-by: tuananh-ng <158744840+tuananh-ng@users.noreply.github.com>
Co-authored-by: Ghost of Sparta <makesocialfoss32@keemail.me>
Co-authored-by: Максим Горпиніч <mgorpinic2005@gmail.com>
Co-authored-by: translatorforkr <tsgjklvfdyu@onionmail.com>
Co-authored-by: Rafi <rafimuhmad90@gmail.com>
Co-authored-by: zenobit <zenobit@users.noreply.hosted.weblate.org>
Co-authored-by: Miyu Sakatsuki <miyu-sakatsuki@outlook.jp>
2024-12-03 15:22:41 +00:00
spaced4ndy a0b8cf62be android, desktop: support business addresses and chats (#5302) 2024-12-03 17:27:00 +04:00
Narasimha-sc 43fa4c43a2 docs: update FAQ (#5179)
* Update FAQ.md

Added:
- Why invite links use simplex.chat domain?
- I do not know my database passphrase

* Update FAQ.md

* Add flatpak directory

* corrections

* correction

* invitation

---------

Co-authored-by: Evgeny <evgeny@poberezkin.com>
2024-12-03 12:48:54 +00:00
Diogo 85e7a13dba desktop: show group message reaction on right click (#5304)
* desktop: show group message reaction on right click

* open on ctrl + click too
2024-12-03 12:26:50 +00:00
Evgeny 9d992735f4 core, ios: improve business address (connection plan, repeat requests, feature items) (#5303)
* core, ios: connection plan for business address

* core: store xcontact_id on business groups to prevent duplicate contact requests

* core: create feature items in new groups and in business groups

* fix tests

* error message
2024-12-03 12:11:38 +00:00
Stanislav Dmitrenko e61babdc8f android, desktop: preserving long message when failed to send (#5297)
* android, desktop: preserving long message when failed to send

* forwarding

* unused code

* strings

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-12-02 23:36:48 +00:00
Diogo c04e952620 desktop: onboarding improvements (#5294)
* consistent space to bottom on future of messaging

* consistent button suze on server operators

* updated setup database passphrase screen

* ability to cancel random passphrase

* reduce conditions padding to header

* show scrollbar in desktop

* EOLs

* EOL

* fix random passphrase param when deleting database and recreating new one

---------

Co-authored-by: Evgeny <evgeny@poberezkin.com>
Co-authored-by: Avently <7953703+avently@users.noreply.github.com>
2024-12-02 21:00:55 +00:00
Diogo 92967dfe0c ios: disable autocorrect add group member search (#5301) 2024-12-02 20:14:26 +00:00
spaced4ndy bc96000131 ios: support business addresses and chats (#5300)
* ios: support business addresses and chats

* improve

* words

* fix

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-12-02 17:40:22 +00:00
Stanislav Dmitrenko a588e7003d android: alert round corners (#5299)
Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-12-02 16:33:22 +00:00
Stanislav Dmitrenko 665501026d android: show info for Xiaomi users about autostart restrictions (#5295)
* android: show info for Xiaomi users about autostart restrictions

* text and placement

* update strings

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-12-02 16:32:04 +00:00
spaced4ndy a62ce9168e core: fix names created for business request group and member (#5296) 2024-12-02 20:30:05 +04:00
Stanislav Dmitrenko f6b611aa30 android, desktop: check existence before deleting database (#5298) 2024-12-02 15:58:21 +00:00
Stanislav Dmitrenko 5a59fdd91c android, desktop: fix Can't represent a width ... and a height ... in Constraints (#5293) 2024-12-02 15:28:58 +00:00
Evgeny 5f01dc1a3f core: support business addresses and chats (#5272)
* core: support business addresses and chats

* types

* connect plan, add link type

* ios: toggle on address UI

* make compile

* todo

* fix migration

* types

* comments

* fix

* remove

* fix schema

* comment

* simplify

* remove diff

* comment

* comment

* diff

* acceptBusinessJoinRequestAsync wip

* comment

* update

* simplify types

* remove business

* wip

* read/write columns

* createBusinessRequestGroup

* remove comments

* read/write business_address column

* validate that business address is not set to be incognito

* replace contact card

* update simplexmq

* refactor

* event when accepting business address request

* sendGroupAutoReply

* delete contact request earlier

* test, fix

* refactor

* refactor2

---------

Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
2024-12-02 14:01:23 +00:00
Evgeny Poberezkin c488c4fcd5 6.2-beta.3: ios 249, android 254, desktop 78 2024-12-01 19:09:53 +00:00
Evgeny Poberezkin 3143cc960e core: 6.2.0.3 2024-12-01 13:18:57 +00:00
Evgeny b8442d92a4 core: improve performance of marking chat items as read (#5290)
* core: improve performance of marking chat items as read

* fix tests
2024-12-01 13:11:30 +00:00
Evgeny Poberezkin 98a3437f43 6.2-beta.2: ios 248, android 253, desktop 77 2024-11-30 22:26:05 +00:00
Evgeny Poberezkin 8f32c6a61a core: 6.2.0.2 2024-11-30 20:54:39 +00:00
Evgeny 79d5573169 cli: fix option --yes-migrate to confirm up migrations automatically, closes #5200 (#5286)
* fix(cli): option to confirm up migrations didn't work

fix #5200

* diff

* import

---------

Co-authored-by: mervyn <6359152+reply2future@users.noreply.github.com>
2024-11-30 20:51:35 +00:00
Evgeny Poberezkin a9e7635e00 core: disable Flux XFTP servers to prevent unknown server warning for the previous version users 2024-11-30 20:15:11 +00:00
Evgeny Poberezkin 2c0de36439 ios: large Conditions screen heading during onboarding 2024-11-30 18:44:59 +00:00
Diogo 94377d0b7a android, desktop: bottom bar and update texts in onboarding (#5279)
* android, desktop: remove one hand ui bar from onboarding and design matching latest ios

* padding before text

* stop reserving space in conditions view

* notifications view

* revert unwanted

* update heading

* translations for new how it works

* how it works redone

* show create profile in how it works

* revert

* conditions of use same padding bottom

* unused str

* swapped instant and off notifications order

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-11-30 18:21:48 +00:00
Evgeny Poberezkin e9853fe3fc ios: update alert message for SimpleX address card 2024-11-30 18:06:36 +00:00
Stanislav Dmitrenko 4738286f4e android, desktop: start/stop chat toggle refactoring (#5261)
* android, desktop: start/stop chat toggle refactoring

* changes

* translations

* better

* better

* do not start chat after export, always show run toggle (#5283)

* update heading

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-11-30 16:33:38 +00:00
Stanislav Dmitrenko 961bdbfc59 ios: start/stop chat toggle refactoring (#5275)
* ios: start/stop chat toggle refactoring

* changes

* changes

* return back

* reduce diff

* better

* update button

* ios: do not start chat after export, always show run toggle (#5284)

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-11-30 16:29:27 +00:00
Evgeny 879c117269 Merge pull request #5259 from simplex-chat/reaction-members
view member reactions
2024-11-30 12:25:33 +00:00
Diogo 03bc4e5d01 ios: display reactions in groups by member (#5265)
* ios: display reactions in groups by member

* fetch data

* load on open

* wip

* fix text

* less api calls

* matching image sizes

* updates

* progress dump

* mostly works

* add member to list needed

* open member faster

---------

Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-11-30 12:23:51 +00:00
Evgeny Poberezkin 80bd4cd337 Merge branch 'master' into reaction-members 2024-11-29 19:40:58 +00:00
Diogo b0f3f0a523 ios: fix alignment on operators review later button and notice (#5280) 2024-11-29 20:04:29 +04:00
Diogo 9915e5572e desktop, android: show group reactions (#5277)
* desktop, android: show group reactions

* handle long names in single line

* swap ordering for composable item action

* Revert "swap ordering for composable item action"

This reverts commit 385825e7f2.

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-11-29 11:19:11 +00:00
spaced4ndy 9c6e0a7051 desktop: fix avatar crop (#5271)
* desktop: fix avatar crop wip

* fix
2024-11-28 13:37:52 +00:00
spaced4ndy 68be4b4ba5 core: return member records from apiGetReactionMembers (#5270) 2024-11-28 13:49:20 +04:00
spaced4ndy 13efdf2595 core: apiGetReactionMembers api implementation (#5263) 2024-11-28 11:24:29 +04:00
Evgeny c3991aad87 website: translations corrections
* Translated using Weblate (French)

Currently translated at 98.8% (254 of 257 strings)

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

* Translated using Weblate (German)

Currently translated at 98.8% (254 of 257 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 98.8% (254 of 257 strings)

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

* Translated using Weblate (Czech)

Currently translated at 98.8% (254 of 257 strings)

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

* Translated using Weblate (Arabic)

Currently translated at 98.8% (254 of 257 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 98.8% (254 of 257 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 98.8% (254 of 257 strings)

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

* Translated using Weblate (Ukrainian)

Currently translated at 98.8% (254 of 257 strings)

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

* Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 98.8% (254 of 257 strings)

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

* Translated using Weblate (Portuguese (Brazil))

Currently translated at 98.0% (252 of 257 strings)

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

* Translated using Weblate (Polish)

Currently translated at 98.8% (254 of 257 strings)

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

* Translated using Weblate (Japanese)

Currently translated at 98.8% (254 of 257 strings)

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

* Translated using Weblate (Russian)

Currently translated at 99.2% (255 of 257 strings)

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

* Translated using Weblate (Hebrew)

Currently translated at 98.8% (254 of 257 strings)

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

* Translated using Weblate (Finnish)

Currently translated at 96.4% (248 of 257 strings)

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

* Translated using Weblate (Hungarian)

Currently translated at 98.8% (254 of 257 strings)

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

---------

Co-authored-by: Anonymous <noreply@weblate.org>
2024-11-28 00:12:53 +00:00
Evgeny 614846465f website: translations (#5266)
* ep/blog-v61

* Translated using Weblate (Hungarian)

Currently translated at 100.0% (257 of 257 strings)

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

---------

Co-authored-by: summoner001 <summoner@vivaldi.net>
2024-11-27 23:51:51 +00:00
Evgeny 096dec2c7b ui: translations (#5264)
* Translated using Weblate (Spanish)

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

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

* Translated using Weblate (Czech)

Currently translated at 96.1% (2008 of 2089 strings)

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

* Translated using Weblate (Hungarian)

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

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

* Translated using Weblate (Hungarian)

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

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

* Translated using Weblate (Arabic)

Currently translated at 100.0% (2089 of 2089 strings)

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

* Translated using Weblate (Vietnamese)

Currently translated at 44.0% (921 of 2089 strings)

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

* Translated using Weblate (Ukrainian)

Currently translated at 100.0% (2089 of 2089 strings)

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

* Translated using Weblate (Hungarian)

Currently translated at 100.0% (2089 of 2089 strings)

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

* Translated using Weblate (Vietnamese)

Currently translated at 44.8% (936 of 2089 strings)

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

* Translated using Weblate (Indonesian)

Currently translated at 12.7% (267 of 2089 strings)

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

* Translated using Weblate (Hungarian)

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

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

* Translated using Weblate (Vietnamese)

Currently translated at 45.7% (956 of 2089 strings)

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

* Translated using Weblate (Korean)

Currently translated at 46.2% (967 of 2089 strings)

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

* Translated using Weblate (Hungarian)

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

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

* Translated using Weblate (Japanese)

Currently translated at 89.4% (1869 of 2089 strings)

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

* Translated using Weblate (Hungarian)

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

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

* Translated using Weblate (Vietnamese)

Currently translated at 46.9% (980 of 2089 strings)

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

* Translated using Weblate (Indonesian)

Currently translated at 12.8% (268 of 2089 strings)

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

* Translated using Weblate (French)

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

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

* Translated using Weblate (Hungarian)

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

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

* Translated using Weblate (Vietnamese)

Currently translated at 51.9% (1086 of 2089 strings)

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

* Translated using Weblate (Vietnamese)

Currently translated at 52.8% (1104 of 2089 strings)

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

* Translated using Weblate (Hungarian)

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

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

* Translated using Weblate (Ukrainian)

Currently translated at 96.6% (1782 of 1843 strings)

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (2089 of 2089 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% (1843 of 1843 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 92.3% (1929 of 2089 strings)

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

* Translated using Weblate (Portuguese (Brazil))

Currently translated at 98.9% (2068 of 2089 strings)

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

* Translated using Weblate (Portuguese (Brazil))

Currently translated at 50.0% (923 of 1843 strings)

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

* Translated using Weblate (Greek)

Currently translated at 19.0% (397 of 2089 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% (2089 of 2089 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% (1843 of 1843 strings)

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

* Translated using Weblate (Vietnamese)

Currently translated at 55.2% (1155 of 2089 strings)

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

* Translated using Weblate (Hungarian)

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

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

* Translated using Weblate (Vietnamese)

Currently translated at 56.2% (1175 of 2089 strings)

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

* Translated using Weblate (Korean)

Currently translated at 51.1% (1068 of 2089 strings)

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

* Translated using Weblate (Hungarian)

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

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

* Translated using Weblate (Korean)

Currently translated at 60.8% (1271 of 2089 strings)

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

* Translated using Weblate (Hungarian)

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

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

* Translated using Weblate (Vietnamese)

Currently translated at 58.0% (1213 of 2089 strings)

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

* Translated using Weblate (Korean)

Currently translated at 63.0% (1317 of 2089 strings)

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

* Translated using Weblate (Indonesian)

Currently translated at 16.1% (337 of 2089 strings)

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

* Translated using Weblate (Japanese)

Currently translated at 92.6% (1936 of 2089 strings)

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

* Translated using Weblate (Korean)

Currently translated at 66.7% (1394 of 2089 strings)

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

* Translated using Weblate (Vietnamese)

Currently translated at 59.8% (1251 of 2089 strings)

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

* Translated using Weblate (Indonesian)

Currently translated at 54.6% (1141 of 2089 strings)

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

* Translated using Weblate (Korean)

Currently translated at 66.7% (1395 of 2089 strings)

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

* Translated using Weblate (Indonesian)

Currently translated at 61.0% (1275 of 2089 strings)

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

* Translated using Weblate (Vietnamese)

Currently translated at 60.8% (1271 of 2089 strings)

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

* Translated using Weblate (Korean)

Currently translated at 67.6% (1414 of 2089 strings)

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

* Translated using Weblate (Hungarian)

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

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

* Translated using Weblate (Vietnamese)

Currently translated at 61.7% (1291 of 2089 strings)

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

* Translated using Weblate (Vietnamese)

Currently translated at 62.6% (1308 of 2089 strings)

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

* Translated using Weblate (Lithuanian)

Currently translated at 83.6% (1747 of 2089 strings)

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

* Translated using Weblate (Vietnamese)

Currently translated at 62.7% (1310 of 2089 strings)

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

* Translated using Weblate (Indonesian)

Currently translated at 61.2% (1279 of 2089 strings)

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

* Translated using Weblate (Vietnamese)

Currently translated at 63.6% (1330 of 2089 strings)

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

* Translated using Weblate (Vietnamese)

Currently translated at 64.3% (1344 of 2089 strings)

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

* process localizations

* ru

* export localizations

---------

Co-authored-by: No name <CertainBot@users.noreply.hosted.weblate.org>
Co-authored-by: M1K4 <oomikaoo@gmail.com>
Co-authored-by: zenobit <zen@osowoso.xyz>
Co-authored-by: summoner001 <summoner@vivaldi.net>
Co-authored-by: jonnysemon <jonnysemon@users.noreply.hosted.weblate.org>
Co-authored-by: tuananh-ng <158744840+tuananh-ng@users.noreply.github.com>
Co-authored-by: Bezruchenko Simon <worcposj44@gmail.com>
Co-authored-by: billy appetie <billy_appetie@users.noreply.hosted.weblate.org>
Co-authored-by: 장재원 <jaeone22@proton.me>
Co-authored-by: Miyu Sakatsuki <miyu-sakatsuki@outlook.jp>
Co-authored-by: Ophiushi <41908476+ishi-sama@users.noreply.github.com>
Co-authored-by: aquaticexpectancy <alexaratovskyi@gmail.com>
Co-authored-by: Random <random-r@users.noreply.hosted.weblate.org>
Co-authored-by: Dampuzakura <dampuzakura@users.noreply.hosted.weblate.org>
Co-authored-by: Luis Henrique Rodrigues Dos Santos <luishenriquesc2020@gmail.com>
Co-authored-by: Dinos B <dinos.nng@gmail.com>
Co-authored-by: translatorforkr <tsgjklvfdyu@onionmail.com>
Co-authored-by: d4f5409d <d4f5409d-4b6a-4640-9ff3-155ebcdc7ab7@anonaddy.me>
Co-authored-by: Rafi <rafimuhmad90@gmail.com>
Co-authored-by: Vaclovas Intas <Gateway_31@protonmail.com>
2024-11-27 23:42:02 +00:00
Diogo d19708ed77 Merge branch 'master' into reaction-members 2024-11-27 22:09:58 +00:00
Diogo 22d7db89d8 ios: database error screens redesign (#5256)
* ios: database error screens redesign (wip)

* refactor

* remove code to simulate errors

* fix

* fix texts

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-11-27 20:32:18 +00:00
Evgeny ba7abcf6f7 ios: update onboarding texts (#5255)
* ios: update onboarding texts

* translations

* more translations

* more translations 2
2024-11-27 19:01:16 +00:00
spaced4ndy b512d3a743 Merge branch 'master' into reaction-members 2024-11-27 20:09:37 +04:00
spaced4ndy 9fa968a593 ui: fix marking chat read (don't use range api) (#5257) 2024-11-27 18:30:39 +04:00
spaced4ndy 41b7ad01f9 core: apiGetReactionMembers (#5258) 2024-11-27 15:51:35 +04:00
spaced4ndy 15fae29e5b android, desktop: offer to create 1-time link on address views (#5253) 2024-11-27 11:16:22 +04:00
Stanislav Dmitrenko 8c1abcccfb android, desktop: scroll to quoted item without known id (#5254) 2024-11-26 14:22:24 +00:00
Stanislav Dmitrenko 25893177d0 ios: view conditions as markdown (#5248)
* ios: view conditions as markdown

* changes

* removed Down

* refactor

* unused

* react on theme change
2024-11-26 13:00:39 +00:00
Diogo 345e0acdec ios: onboarding redesign (#5252)
* ios: onboarding redesign

* shorter texts

* updates

* more updates

* remove extra padding when focused

* strings

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-11-26 12:26:35 +00:00
spaced4ndy 1f04984a34 ios: offer to create 1-time link on address views (#5249) 2024-11-26 14:43:39 +04:00
Stanislav Dmitrenko 7a91ed2ab2 android, desktop: view conditions as markdown (#5247)
* android, desktop: view conditions as markdown

* better animation

* unused

* open chat links inside the app and removed divider, smaller font

* paddings
2024-11-25 16:20:02 +00:00
spaced4ndy d912fe07a1 core: fix pagination indexes (#5241) 2024-11-25 18:51:49 +04:00
Evgeny cfc21dfb51 ios: address or 1-time link (#5246) 2024-11-25 18:15:32 +04:00
spaced4ndy e5c83b20c9 android, desktop: fix operator disabled indication (#5242) 2024-11-25 15:52:30 +04:00
Evgeny 97b472fd9c blog: operators (#5240)
* blog: network operators (draft)

* update

* update

* ui: update whats new link

* fix file name

* update

* update

* update

* update
2024-11-25 09:24:12 +00:00
Stanislav Dmitrenko d40d690f86 desktop (Windows): fix linking with openssl 3 (#5238) 2024-11-24 08:27:58 +00:00
Evgeny Poberezkin 6581e27524 6.2-beta.1: ios 247, android 252, desktop 76 2024-11-23 17:42:24 +00:00
Diogo 909edac64f desktop: unsaved changes popup for network and servers when clicking middle lane (#5230)
* Revert "Revert "handle click when have unsaved changes""

This reverts commit ba53cc63c6.

* fix in children view

* unsaved changes for network and children

* don't close all modals when pressing back

* explicit param

---------

Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
2024-11-23 14:42:25 +00:00
Evgeny 30a24df9c0 ui: update whats new link (#5234)
* ui: update whats new link

* fix file name
2024-11-23 14:37:44 +00:00
Evgeny Poberezkin 7bcb514baf core: 6.2.0.1 (simplexmq: 6.2.0.4) 2024-11-23 11:43:52 +00:00
Stanislav Dmitrenko bda84b08a1 ci: fix mac & Windows build (#5232)
* core: 6.2.0.1 (simplexmq 6.2.0.4)

* action: fix mac build

* fix Windows

* version

* revert version change

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-11-23 11:41:48 +00:00
Evgeny 2f0fe50f88 Merge pull request #5111 from simplex-chat/server-operators
core, ui: add support for server operators
2024-11-22 19:26:57 +00:00
Evgeny 4f640c96d1 build: use openssl 3.0 (#5183)
* build: use openssl 3.0

* docs

* mac script
2024-11-22 18:38:49 +00:00
Evgeny 9b71702ac8 ios: move onboarding action cards, paddings (#5231) 2024-11-22 18:19:49 +00:00
Evgeny Poberezkin 76aedb4a15 core: update simplexmq 2024-11-22 17:21:05 +00:00
Evgeny Poberezkin a6f5ba541b android, desktop: smaller info icon, corrections 2024-11-22 16:43:10 +00:00
spaced4ndy e47b16f3b4 android: improve layout of operator logo 2024-11-22 19:40:41 +04:00
Evgeny Poberezkin 494ef6e671 Merge branch 'master' into server-operators 2024-11-22 15:35:43 +00:00
Stanislav Dmitrenko bff2d7d3b6 android, desktop: highlight quoted messaged on click to scroll to it (#5229) 2024-11-22 15:34:43 +00:00
spaced4ndy 2adfa0c18b android: information icon right of operator logo 2024-11-22 19:33:49 +04:00
spaced4ndy b5170684ad android: fix single operator conditions paddings 2024-11-22 19:21:21 +04:00
Diogo 396fa7f988 desktop, android: server operators (#5212)
* api and types

* whats new view

* new package and movements

* move network and servers to new package

* network and servers view

* wip

* api update

* build

* conditions modal in settings

* network and servers fns

* save server fixes

* more servers

* move protocol servers view

* message servers with validation

* added message servers

* use for files

* fix error by server type

* list xftp servers

* android: add server view (#5221)

* android add server wip

* test servers button

* fix save of custom servers

* remove unused code

* edit and view servers

* fix

* allow to enable untested

* show all test errors in the end

* android: custom servers view (#5224)

* cleanup

* validation footers

* operator enabled validation

* var -> val

* reuse onboarding button

* AppBarTitle without alpha

* remove non scrollable title

* change in AppBarTitle

* changes in AppBar

* bold strings + bordered text view

* ChooseServerOperators

* fix

* new server view wip

* fix

* scan

* rename

* fix roles toggle texts

* UsageConditionsView

* aligned texts

* more texts

* replace hard coded logos with object ref

* use snapshot state to recalculate errors

* align views; fix accept

* remove extra snapshots

* fix ts

* fix whatsnew

* stage

* animation on onboarding

* refactor and fix

* remember

* fix start chat alert

* show notice in chat list

* refactor

* fix validation

* open conditions

* whats new view updates

* icon for navigation improvements

* remove debug

* simplify

* fix

* handle click when have unsaved changes

* fix

* Revert "fix"

This reverts commit d49c373641.

* Revert "handle click when have unsaved changes"

This reverts commit 39ca03f9c0.

* fixed close of modals in whats new view

* grouping

* android: conditions view paddings (#5228)

* revert padding

* refresh operators on save

* fixed modals in different views for desktop

* ios: fix enabling operator model update

* fix modals

---------

Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
Co-authored-by: Avently <7953703+avently@users.noreply.github.com>
2024-11-22 18:42:07 +04:00
Stanislav Dmitrenko ea9ee987cf android, desktop: better message info screen (#5227)
- changed tabBar style to leading icon
- made tabBar the same size as AppBars
- made background color as theme background
2024-11-22 12:59:39 +00:00
Evgeny 49d1b26bba core: tests for operators api, CLI command to update operators (#5226) 2024-11-22 10:38:00 +00:00
spaced4ndy bab63d8f27 ios: fix repeatedly showing updated conditions 2024-11-22 13:23:33 +04:00
sh 1083a0727a flatpak: update metainfo (#5146) 2024-11-22 12:31:58 +04:00
Evgeny 78b3b12ec1 ios: button to open conditions and changes (#5225) 2024-11-21 21:02:55 +04:00
Evgeny Poberezkin 61d7df8906 ui: always use private routing by default 2024-11-21 16:54:35 +00:00
Evgeny 522f99aadd directory service: notify admins about group registration events (#5223) 2024-11-20 22:39:13 +00:00
Evgeny Poberezkin 927a04d45f Merge branch 'master' into server-operators 2024-11-20 19:24:55 +00:00
Stanislav Dmitrenko 2b155db57d android, desktop: open chat on first unread, "scroll" to quoted items that were not loaded (#5140)
* android, desktop: infinity scroll rework

* group corrections

* scroll to quote/unread/top/bottom

* changes

* changes

* changes

* changes

* better

* changes

* fix chat closing on desktop

* fix reading items counter, scrolling to newly appeared message, removed unneeded items loading, only partially visible items marked read

* workaround of showing buttom with arrow down on new messages receiving

* rename param

* fix tests

* comments and removed unused code

* performance optimization

* optimization for loading more items in small chat

* fix loading prev items in loop

* workaround to blinking button with counter

* terminal scroll fix

* different click events for floating buttons

* refactor

* change

* WIP

* refactor

* refactor

* renames

* refactor

* refactor

* change

* mark read problem fix

* fix tests

* fix auto scroll in some situations

* fix scroll to quote when it's near the top loaded area

* refactor

* refactor

* rename

* rename

* fix

* alert when quoted message doesn't exist

* refactor

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-11-20 19:23:55 +00:00
spaced4ndy f3cef7ce12 ios: remove unused type 2024-11-20 18:23:51 +04:00
spaced4ndy 29b54ec5b2 ios: rework saving settings (#5219)
* ios: rework saving settings

* fix

* shorter names

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-11-20 17:58:13 +04:00
spaced4ndy 313acefb19 ios: remove crashing accept button (#5217) 2024-11-20 17:18:24 +04:00
spaced4ndy e5534c0402 ios: improve onboarding animations (#5216) 2024-11-20 14:28:36 +04:00
Evgeny 4e37efdc4a core: update agent servers (#5215) 2024-11-20 11:23:25 +04:00
spaced4ndy 58c92ed004 ios: rework existing users notice, condition views (#5214) 2024-11-19 20:48:51 +04:00
Evgeny 181f72fa1f ios: texts about operators (#5213)
* ios: texts about operators

* remove comment

* button for conditions
2024-11-19 19:26:41 +04:00
Evgeny 4b9c618ae3 core: remove a separate type to validate servers with invalid addresses (they are prevented by the UI) (#5211) 2024-11-19 14:10:33 +00:00
spaced4ndy 70a29512b7 ios: server operators ui (#5114)
* wip

* refactor, fix bindings

* wip

* wip

* fixes

* wip

* information map, logos

* global conditions hack

* restructure

* restructure

* texts

* text

* restructure

* wip

* restructure

* rename

* wip

* conditions for all

* comment

* onboarding wip

* onboarding wip

* fix paddings

* fix paddings

* wip

* fix padding

* onboarding wip

* nav link instead of sheet

* pretty button

* large titles

* notifications mode button style

* reenable demo operator

* Revert "reenable demo operator"

This reverts commit 42111eb333.

* padding

* reenable demo operator

* refactor (removes additional model api)

* style

* bold

* bold

* light/dark

* fix button

* comment

* wip

* remove preset

* new types

* api types

* apis

* smp and xftp servers in single view

* test operator servers, refactor

* save in main view

* better progress

* better in progress

* remove shadow

* update

* apis

* conditions view wip

* load text

* remove custom servers button from onboarding, open already conditions in nav link

* allow to continue with simplex on onboarding

* footer

* existing users notice

* fix to not show nothing on no action

* disable notice

* review later

* disable notice

* wip

* wip

* wip

* wip

* optional tag

* fix

* fix tags

* fix

* wip

* remove coding keys

* fix onboarding

* rename

* rework model wip

* wip

* wip

* wip

* fix

* wip

* wip

* delete

* simplify

* wip

* fix delete

* ios: server operators ui wip

* refactor

* edited

* save servers on dismiss/back

* ios: add address card and remove address from onboarding (#5181)

* ios: add address card and remove address from onboarding

* allow for address creation in info when open via card

* conditions interactions wip

* conditions interactions wip

* fix

* wip

* wip

* wip

* wip

* rename

* wip

* fix

* remove operator binding

* fix set enabled

* rename

* cleanup

* text

* fix info view dark mode

* update lib

* ios: operators & servers validation

* fix

* ios: align onboarding style

* ios: align onboarding style

* ios: operators info (#5207)

* ios: operators info

* update

* update texts

* texts

---------

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

---------

Co-authored-by: Diogo <diogofncunha@gmail.com>
Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-11-19 11:37:00 +00:00
spaced4ndy fcae5e9925 core: fix validation of operator servers for non current users (#5205)
* core: fix validation of operator servers for non current users

* style

* refactor

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-11-18 20:22:35 +00:00
Evgeny 619985730e core: use random servers for each operator (#5192)
* core: use random servers for each operator (WIP, compiles with undefined stub)

* compiles

* fix some, break some

* tests pass

* cleanup

* delays in tests

* enable random servers test

* remove new preset servers in down migration

* fix migration

* test
2024-11-18 18:44:28 +00:00
Evgeny Poberezkin 3d4a47cdae Merge branch 'master' into server-operators 2024-11-18 16:55:07 +00:00
Diogo d1ae3ba2d3 desktop, android: add address card to chat list and remove address from onboarding (#5177)
* desktop, android: add address card to chat list

* add create address button to address learn more view

* envelope size to match avatars

* refactor

* no color for info icon

* envelope padding

* remove address from onboarding

* show create in address card info

* backwards compatibility for address onboarding step

* paddings between cards

* paddings

* toolbar -> chats -> cards

* dont hide address card

* update string

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-11-18 15:36:54 +00:00
Evgeny Poberezkin feb4ecbb6b Merge branch 'master' into server-operators 2024-11-18 06:54:19 +00:00
Evgeny Poberezkin e645dd99e7 6.2-beta.0: ios 246, android 251, desktop 75 2024-11-17 22:37:18 +00:00
Evgeny Poberezkin a17bfc52ce Merge branch 'master' into server-operators 2024-11-17 11:13:42 +00:00
Evgeny Poberezkin 6843269cff core: 6.2.0.0 (simplexmq: 6.2.0.3) 2024-11-17 11:09:26 +00:00
Evgeny Poberezkin b605ebfd2a core: remove comments 2024-11-15 12:14:53 +00:00
Evgeny feb687d3b8 core: different roles for different protocols (#5185)
* core: different roles for different protocols

* include current conditions in responses

* fix

* fix test

* fix

---------

Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
2024-11-15 12:08:15 +00:00
spaced4ndy ff8e29c0eb core: fix accept conditions query (#5187) 2024-11-15 11:20:32 +04:00
Evgeny 1fbf21d395 core: validate servers of all user profiles (#5180)
* core: validate servers of all user profiles

* validate all servers

* fix parsing, test
2024-11-15 07:15:04 +00:00
Evgeny d42cab8e22 core: preset operators and servers (#5142)
* core: preset servers and operators (WIP)

* usageConditionsToAdd

* simplify

* WIP

* database entity IDs

* preset operators and servers (compiles)

* update (most tests pass)

* remove imports

* fix

* update

* make preset servers lists potentially empty in some operators, as long as the combined list is not empty

* CLI API in progress, validateUserServers

* make servers of disabled operators "unknown", consider only enabled servers when switching profile links

* exclude disabled operators when receiving files

* fix TH in ghc 8.10.7

* add type for ghc 8.10.7

* pattern match for ghc 8.10.7

* ghc 8.10.7 fix attempt

* remove additional pattern, update servers

* do not strip title from conditions

* remove space

---------

Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
2024-11-14 17:43:34 +00:00
Evgeny Poberezkin 807f698cf2 Merge branch 'master' into server-operators 2024-11-14 12:24:53 +00:00
Evgeny Poberezkin e45a96935c ci: update website build 2024-11-14 12:16:51 +00:00
Evgeny a5061f3147 docs: update privacy policy and conditions of use (#5129)
* docs: update privacy policy and conditions of use

* update

* note

* update date
2024-11-14 11:59:44 +00:00
Diogo 4d82209a3a core: pagination API to load items around defined or the earliest unread item (#5100)
* core: auto increment chat item ids (#5088)

* core: auto increment chat item ids

* file name

* down name

* update schema

* ignore down migration on schema dump test

* fix testDirectMessageDelete test

* fix testNotes test

* core: initial api support for items around a given item (#5092)

* core: initial api support for items around a given item

* implementation and tests for local messages

* pass entities down

* unused

* getAllChatItems implementation and tests

* pagination for getting chat and tests

* remove unused import

* group implementation and tests

* refactor

* order by created at for local and direct chats

* core: initial landing api for chat and gaps  (#5104)

* initial work on initial param for loading chat

* support for initial

* controller parse

* fixed sqls

* refactor names

* fix ChatLandingSection serialized type

* total accuracy on landing section

* descriptive view message

* foldr

* refactor to make landingSection reusable

* refactor: use foldr everywhere

* propagate search

* Revert "propagate search"

This reverts commit 01611fd719.

* throw when search is sent for initial

* gap size wip (needs testing)

* final

* remove order by

* remove index

---------

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

* core: fix initial api latest chat items ordering (#5151)

* core: fix one item missing from latest in initial and wrong check (#5153)

* core: fix one item missing from latest in initial and wrong check

* final fixes and tests

* clearer tests

* core: remove gaps and make sure page size is always the same (#5163)

* remove gaps

* consistent pagination size

* proper fix and around fix too

* optimize

* refactor

* core: simplify pagination

* core: first unread queries (#5174)

* core: pagination nav info (#5175)

* core: pagination nav info

* wip

* rework

* rework

* group, local

* fix

* rename

* fix tests

* just

---------

Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-11-14 08:34:25 +00:00
Diogo 60c37f0d1d ios: user profiles move auth to change actions, show unread counts (#5170)
* ios: user profiles move auth to change actions, show unread count per profile

* simpler approach and add profile protection

* not show muted icon

* refactor

* not needed

* fix

* simpler fix

* deadline

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-11-13 11:41:39 +00:00
Diogo 15bac88ec9 desktop, android: user profiles move auth to change actions, show unread counts (#5171)
* auth only on change actions for profiles

* show notification count in profiles view

* auth to hidde profile

* save authorized

* refactor and icon fix

* keep key
2024-11-13 09:27:49 +00:00
sh 8af54539f6 docs: add control port section (#5164)
* docs: add control port section

* docs: apply suggestions
2024-11-12 10:37:12 +00:00
Evgeny Poberezkin 457e12880c Merge branch 'master' into server-operators 2024-11-10 16:15:07 +00:00
Evgeny 2d588949b1 directory service: additional commands (#5159)
* directory service: additional commands

* notify superusers

* 48 hours

* replace T.elem
2024-11-10 15:21:33 +00:00
spaced4ndy ef0f21a11c core: operator apis commands (#5155) 2024-11-08 14:45:00 +04:00
spaced4ndy 8396e70e7b core: validate servers - find servers with duplicate hosts (#5150) 2024-11-06 16:13:08 +04:00
spaced4ndy 2da89c2cf1 core: setConditionsNotified, acceptConditions, setUserServers, validateServers apis wip (#5147) 2024-11-05 21:40:33 +04:00
spaced4ndy 3b0205b25f core: setServerOperators, getUsageConditions api wip (#5145) 2024-11-05 14:15:20 +04:00
spaced4ndy bdaec30fa0 core: getServerOperators, getUserServers, getUsageConditions apis wip (#5141) 2024-11-04 21:11:03 +04:00
Evgeny 97df069730 core: add support for server operators (#4961)
* core: add support for server operators

* migration

* update schema and queries, rfc

* add usage conditions tables

* core: server operators new apis draft

* update

* conditions

* update

* add get conditions api

* add get conditions API

* WIP

* compiles

* fix schema

* core: ui logic in types (#5139)

* update

---------

Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
2024-11-04 13:28:57 +00:00
Evgeny Poberezkin 9a1166f097 Merge branch 'master' into server-operators 2024-11-03 09:12:12 +00:00
Evgeny Poberezkin 7a741e7ac4 ios: update core library 2024-11-02 20:03:27 +00:00
Alexander Bondarenko 165143a111 Use simplexmq with client_library flag (#5133)
* Use simplexmq with client_library flag

* fix server config for mq master

* simplexmq

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-11-02 17:51:11 +00:00
Evgeny ceb17b23b4 bumped haskell.nix (#5134)
Co-authored-by: Moritz Angermann <moritz.angermann@gmail.com>
2024-11-02 15:28:41 +00:00
Evgeny Poberezkin 3c8c9d8b52 website: update jobs page 2024-11-02 13:43:45 +00:00
Stanislav Dmitrenko 4162bccc46 multiplatform: edge to edge design (#5051)
* multiplatform: insets

* more features and better performance

* calls and removed unused code

* changes

* removed logs

* status and nav bar colors

* chatList and newChatSheet search fields

* overhaul

* search fields, devtools, chatlist, newchatsheet, onehand on desktop, scrollbars

* android, desktop: update to Compose 1.7.0

- support image drag-and-drop from other applications right to a chat
(with and without transparent pixels - will be png or jpg)

* stable

* workaround

* changes

* ideal adapting height layout

* dropdownmenu, userpicker, onehandui, call layout, columns

* rename bars properties and strings

* faster update and better layout

* gallery in landscape with cutout

* better cutout

* 1% step on slider

* app bar moves to bottom in one hand ui

* default alpha

* changes

* userpicker colors

* changes

* blur

* fix wrong drawing area in chatview

* fix

* fixed differently

* changes

* changes

* android fix

* Revert "android fix"

This reverts commit 7d417afd9b.

* changes

* changes

* blur

* swap

* no logs

* fix build

* old Android support

* fix position of menu

* disable blur on Android 12

* call button padding

* useless code

* fix padding in group info view

* rename

* rename

* newline

* one more fix

* changes

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-10-31 17:26:17 +00:00
Stanislav Dmitrenko 24090fe350 android, desktop: update to Compose 1.7.0 (#5038)
* docs: correction

* android, desktop: update to Compose 1.7.0

- support image drag-and-drop from other applications right to a chat
(with and without transparent pixels - will be png or jpg)

* stable

* workaround

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-10-31 17:11:26 +00:00
spaced4ndy 37b78edb91 ios: move Network and servers settings modules to folder (#5110) 2024-10-28 18:18:26 +04:00
spaced4ndy 78510b6fd3 core, ios: get messages for multiple last notifications; separately get notification connections before requesting messages (to avoid acknowledgement races in case of parralel nse threads); coordinate nse threads (#5084)
* core, ios: get messages for multiple last notifications (#5047)

* ios: refactor notification service (#5086)

* core, ios: separately get notification connections before requesting messages; coordinate nse threads (#5085)
2024-10-25 20:09:59 +04:00
Evgeny Poberezkin 1730c5136f Merge branch 'stable' into stable-android 2024-10-18 19:20:22 +01:00
Evgeny Poberezkin edf99fcd1d 6.1.1: ios 245, android 249, desktop 74 2024-10-18 18:37:14 +01:00
spaced4ndy 2ffabd1ef8 ios: fix changing user via notification (#5069) 2024-10-18 18:07:38 +04:00
Evgeny Poberezkin 28383edb83 core: 6.1.1.0 (simplexmq: 6.1.1.0) 2024-10-18 14:21:17 +01:00
Evgeny 9175897acf core, ui: add SMP STORE error (#5071)
* core, ui: add SMP STORE error

* update library
2024-10-18 14:17:04 +01:00
Evgeny Poberezkin f3cd167502 core: ntf server 2024-10-18 13:06:47 +01:00
Arturs Krumins 7cde2cf6c2 ios: optimise ComposeView rendering (#5042)
* ios: replace revealed bindings with constant value

* ios: optimise ComposeView rendering

* rename

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-10-18 12:35:27 +01:00
Arturs Krumins 3913043705 ios: fix chat not loading if initial page has too many merged items (#5066)
Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-10-18 10:07:18 +01:00
Arturs Krumins a160acef12 ios: fix navigation title redaction after biometric authentication (#5065) 2024-10-18 10:04:53 +01:00
Arturs Krumins c54fae0136 ios: fix sheets dismissing during biometric authentication (#5062)
* ios: fix sheets dismissing during biometric authentication

* remove AppSheet

* Revert "remove AppSheet"

This reverts commit 3aa1688cbd.

* remove local auth request on sheet dismissal

* revert biometricAuth
2024-10-16 19:55:59 +01:00
Arturs Krumins d57abfcc93 ios: fix theme import file picker (#5048)
* ios: fix theme import file picker

* minor
2024-10-16 19:48:13 +01:00
Evgeny 515a0ddfdd blog: wired's attack on privacy (#5063)
* blog: wired misleading attack on privacy of communications

* image

* update

* title

* update

* update

* preview
2024-10-16 19:25:47 +01:00
spaced4ndy b5d8c65249 ui: quota error description (#5037) 2024-10-15 12:01:06 +04:00
Arturs Krumins de94892fe7 ios: replace revealed bindings with constant value (#5027) 2024-10-15 08:58:54 +01:00
Evgeny Poberezkin b7131e16f2 docs: fix links 2024-10-14 13:27:04 +01:00
Evgeny 11a44dc1fd blog: v6.1 and security review announcement (#5040)
* blog: v6.1 and security review announcement

* update, images

* readme

* update review links on home page

* links to review
2024-10-14 13:18:48 +01:00
sh 0af718f03f flatpak: update metainfo (#5039) 2024-10-14 10:12:00 +01:00
Evgeny Poberezkin f8f5c3c6be docs: correction 2024-10-14 09:37:31 +01:00
sh 13912a4af9 docs/smp-server: update to latest changes (#4960)
* docs/smp-server: update to latest changes

* update

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-10-13 09:43:47 +01:00
Evgeny Poberezkin 06bb29e87d Merge branch 'stable' into stable-android 2024-10-12 12:33:16 +01:00
Evgeny Poberezkin 601b4cd619 6.1: ios 244, android 247, desktop 73 2024-10-12 12:08:16 +01:00
Evgeny d2b4b7bed6 ui: translations (#5031)
* Translated using Weblate (Spanish)

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

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

* Translated using Weblate (Czech)

Currently translated at 96.1% (2008 of 2089 strings)

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

* Translated using Weblate (Hungarian)

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

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

* process localizations

---------

Co-authored-by: No name <CertainBot@users.noreply.hosted.weblate.org>
Co-authored-by: M1K4 <oomikaoo@gmail.com>
Co-authored-by: zenobit <zen@osowoso.xyz>
Co-authored-by: summoner001 <summoner@vivaldi.net>
2024-10-12 11:36:49 +01:00
Diogo fa95e4e9ad ios: dont show tails for moderated and blocked items unless revealed (#5030)
* ios: stop showing tails for non revealed moderated or blocked items

* simplify

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-10-12 10:59:51 +01:00
Evgeny Poberezkin 88fdc1ef75 core: 6.1.0.9 2024-10-12 10:56:24 +01:00
Evgeny 7ab6e44a6e directory service: list pending groups (#5029)
* directory service: list pending groups

* user commands to remove a group from directory and to set initial member role (TODO tests)

* tests
2024-10-12 10:33:45 +01:00
Evgeny Poberezkin 26986686ca ios: fix link previews to be enabled by default 2024-10-12 09:06:05 +01:00
spaced4ndy e76dc33cf0 core: associate new contact with all corresponding members on member contact re-creation (e.g. after it was merged to many members and then deleted) (#5028) 2024-10-11 20:47:54 +01:00
Evgeny aa2eafdacb blog: v6.1 announcement placeholder (#5004)
* blog: v6.1 announcement placeholder

* draft
2024-10-11 20:46:08 +01:00
Evgeny Poberezkin 2c3c97f5cc 6.1-beta.5: ios 243, android 246, desktop 72 2024-10-11 18:46:45 +01:00
Evgeny 83f42704ea ui: translations (#5026)
* Translated using Weblate (Italian)

Currently translated at 100.0% (1843 of 1843 strings)

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

* Translated using Weblate (Spanish)

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

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

* Translated using Weblate (Turkish)

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

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

* Translated using Weblate (Hungarian)

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

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

* Translated using Weblate (Hungarian)

Currently translated at 100.0% (2089 of 2089 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% (2089 of 2089 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 99.7% (1839 of 1843 strings)

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

* Translated using Weblate (Hungarian)

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

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

* Translated using Weblate (German)

Currently translated at 100.0% (1843 of 1843 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% (1843 of 1843 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 65.1% (1201 of 1843 strings)

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

* Translated using Weblate (Hungarian)

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

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

* Translated using Weblate (Russian)

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

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

* process localizations

---------

Co-authored-by: Random <random-r@users.noreply.hosted.weblate.org>
Co-authored-by: No name <CertainBot@users.noreply.hosted.weblate.org>
Co-authored-by: Abdullah Koyuncu <wisewebworks@outlook.com>
Co-authored-by: Ghost of Sparta <makesocialfoss32@keemail.me>
Co-authored-by: summoner001 <summoner@vivaldi.net>
Co-authored-by: mlanp <github@lang.xyz>
Co-authored-by: M1K4 <oomikaoo@gmail.com>
Co-authored-by: acevif <acevif@gmail.com>
2024-10-11 17:08:32 +01:00
Stanislav Dmitrenko 7ff6ef09fe android: hide mic icon when mic is disabled (#5025) 2024-10-11 16:54:15 +01:00
Evgeny Poberezkin dbe4504f05 core: 6.1.0.8 (simplexmq: 6.1.0.7) 2024-10-11 15:44:13 +01:00
spaced4ndy 9a87f344b5 core: do not regenerate key when accepting connection to avoid invalidating invitation link on bad networks (#5018)
* core: prepare conn (plan)

* update

* group join

* comment

* comment

* wip

* Revert "wip"

This reverts commit 0849f43377.

* accept

* save contact_id, reuse contact

* refactor

* simplexmq

* set contactUsed

* support retrying join

* exclude prepared connections from API responses

* avoid race with events

* avoid race better

* fix UI

* update library

* tmp

* update

* display error details on ios cmd prohibited

* underscore instead of empty

* Update apps/ios/Shared/Model/SimpleXAPI.swift

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

* test

* update simplexmq

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
Co-authored-by: Diogo <diogofncunha@gmail.com>
2024-10-11 15:37:38 +01:00
Stanislav Dmitrenko 2127c7dcce android, desktop: don't stop audio track on Android in calls (#5024)
* android, desktop: don't stop audio track on Android in calls

There is a problem related to managing selected audio output device in
call. When microphone is disabled, WebView turns speaker on without
need. No way to prevent it was found yet. This is temporary workaround
that makes everything work except it makes microphone icon visible in
status bar (microphone is not used actually in that moment)

* enabled=false
2024-10-11 14:37:36 +01:00
Stanislav Dmitrenko e3528d3ffe android: re-apply custom language when webview appears (#5022)
* android: re-apply custom language when webview appears

There is a bug on Android related to including WebView. App language
changes to system language regardless of what was set before in
context's configuration. Re-set needed to be done twice: after creating
of WebView and after removing it from a view

* add link to bug

---------

Co-authored-by: Evgeny <evgeny@poberezkin.com>
2024-10-11 14:36:57 +01:00
Evgeny Poberezkin ec014d721e sdk: fix test 2024-10-10 19:15:09 +01:00
Yaroslav Pavlov 75bacb7923 desktop: fix typescript sdk ability to send / receive messages (#4970)
* typescript sdk: fix send messages

* typescript sdk: fix send messages naming
2024-10-10 19:10:11 +01:00
Stanislav Dmitrenko 0d8c179861 ios: fix not showing link creation and add group members pages (#5020) 2024-10-10 19:08:03 +01:00
Diogo e9a99dfb3c ios: fix empty qr code reader when swapping to connect via link (#5016) 2024-10-10 19:06:25 +01:00
Diogo baa585357f multiplatform: disable chat buttons on user picker when chat is stopped (#5017)
* ios: disable chat buttons on user picker when chat is stopped

* small change

* disable use from desktop on android when chat stopped
2024-10-10 19:01:31 +01:00
Diogo cebb4aa93b ios: fix ocassional error on getSubsTotal (#5021) 2024-10-10 18:55:37 +01:00
Stanislav Dmitrenko df53ae9d4f ios: fix remaining bugs in calls (#5013) 2024-10-10 12:11:01 +01:00
Evgeny 21b1904b0e ui: translations (#5015)
* Translated using Weblate (Italian)

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

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

* Translated using Weblate (Chinese (Simplified Han script))

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

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

* Translated using Weblate (Ukrainian)

Currently translated at 100.0% (2079 of 2079 strings)

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

* Translated using Weblate (Polish)

Currently translated at 99.2% (2064 of 2079 strings)

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

* Translated using Weblate (Turkish)

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

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

* Translated using Weblate (Polish)

Currently translated at 99.7% (2083 of 2089 strings)

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

* Translated using Weblate (Hungarian)

Currently translated at 99.5% (2079 of 2089 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% (1831 of 1831 strings)

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

* Translated using Weblate (Dutch)

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

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

* Translated using Weblate (Polish)

Currently translated at 99.9% (2088 of 2089 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% (1831 of 1831 strings)

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

* Translated using Weblate (Turkish)

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

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

* Translated using Weblate (Vietnamese)

Currently translated at 43.0% (899 of 2089 strings)

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

* Translated using Weblate (Indonesian)

Currently translated at 12.5% (263 of 2089 strings)

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

* Translated using Weblate (Hungarian)

Currently translated at 100.0% (2089 of 2089 strings)

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

* Translated using Weblate (Vietnamese)

Currently translated at 43.1% (901 of 2089 strings)

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

* Translated using Weblate (Hungarian)

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

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

* Translated using Weblate (Chinese (Simplified Han script))

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

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

* Translated using Weblate (Arabic)

Currently translated at 100.0% (2089 of 2089 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 11.6% (243 of 2089 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.2% (22 of 1831 strings)

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

* Translated using Weblate (Hungarian)

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

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

* Translated using Weblate (Russian)

Currently translated at 100.0% (2089 of 2089 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.9% (1830 of 1831 strings)

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

* Translated using Weblate (Hungarian)

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

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

* process localizations

---------

Co-authored-by: Random <random-r@users.noreply.hosted.weblate.org>
Co-authored-by: 大王叫我来巡山 <hamburger2048@users.noreply.hosted.weblate.org>
Co-authored-by: No name <CertainBot@users.noreply.hosted.weblate.org>
Co-authored-by: Bezruchenko Simon <worcposj44@gmail.com>
Co-authored-by: B.O.S.S <BxOxSxS@protonmail.com>
Co-authored-by: Abdullah Koyuncu <wisewebworks@outlook.com>
Co-authored-by: summoner001 <summoner@vivaldi.net>
Co-authored-by: M1K4 <oomikaoo@gmail.com>
Co-authored-by: tuananh-ng <158744840+tuananh-ng@users.noreply.github.com>
Co-authored-by: billy appetie <billy_appetie@users.noreply.hosted.weblate.org>
Co-authored-by: Ghost of Sparta <makesocialfoss32@keemail.me>
Co-authored-by: mlanp <github@lang.xyz>
Co-authored-by: jonnysemon <jonnysemon@users.noreply.hosted.weblate.org>
Co-authored-by: diodepon <diopon@mailo.com>
Co-authored-by: J R <jr@simplex.chat>
2024-10-10 11:30:51 +01:00
Diogo b1ef442f1e android, desktop: fix profile switching failure handling (#5014) 2024-10-10 14:02:05 +04:00
Evgeny Poberezkin 4020cb074f ios: export localizations 2024-10-10 11:01:15 +01:00
Arturs Krumins 0c69f6553a ios: fix group member sheet load animation (#5008)
* ios: fix group member sheet load animation

* improve diff

* rename

* revert moving sections

* re-indent

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-10-10 10:57:15 +01:00
Evgeny 03865b4a18 ios: v6.1 whats new (#5005)
* ios: v6.1 whats new

* update

* android, desktop
2024-10-09 15:56:05 +01:00
Evgeny ac5f0bc7bb ui: allow deleting and moderating up to 200 messages (#5010) 2024-10-09 12:31:51 +01:00
spaced4ndy 12423f4afa core: test db indexes (#4999) 2024-10-09 15:15:58 +04:00
Stanislav Dmitrenko fabbe0285d ui: rely on different value in stats when checking calls media (#5007)
* ui: rely on different value in stats when checking calls media

* int64
2024-10-09 08:37:21 +01:00
Evgeny Poberezkin 05cc300231 Merge tag 'v6.0.5' into stable-android 2024-10-09 08:28:29 +01:00
Evgeny Poberezkin b01efd9d0a Merge branch 'stable' 2024-10-08 20:55:46 +01:00
Evgeny Poberezkin fc28ff0f15 6.1-beta.4: ios 242, android 245, desktop 71 2024-10-08 19:08:54 +01:00
Evgeny 213f6cf372 website: translations (#5003)
* Translated using Weblate (Hungarian)

Currently translated at 100.0% (257 of 257 strings)

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

* Translated using Weblate (Hungarian)

Currently translated at 100.0% (257 of 257 strings)

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

---------

Co-authored-by: summoner001 <summoner@vivaldi.net>
2024-10-08 18:30:50 +01:00
Evgeny a764e1906d ui: translations (#5002)
* Translated using Weblate (Dutch)

Currently translated at 99.9% (2071 of 2072 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.9% (1822 of 1823 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% (2072 of 2072 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% (2072 of 2072 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% (1823 of 1823 strings)

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

* Translated using Weblate (Hungarian)

Currently translated at 100.0% (1823 of 1823 strings)

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

* Translated using Weblate (German)

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

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

* Translated using Weblate (Chinese (Simplified Han script))

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

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

* Translated using Weblate (Ukrainian)

Currently translated at 100.0% (2072 of 2072 strings)

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

* Translated using Weblate (Portuguese)

Currently translated at 46.2% (958 of 2072 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 99.8% (2069 of 2072 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% (2072 of 2072 strings)

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

* Translated using Weblate (Hungarian)

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

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

* Translated using Weblate (Turkish)

Currently translated at 95.0% (1974 of 2076 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% (2076 of 2076 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 95.9% (1991 of 2076 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% (2076 of 2076 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% (1823 of 1823 strings)

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

* process localizations

* restore "Chat profile" translations

---------

Co-authored-by: M1K4 <oomikaoo@gmail.com>
Co-authored-by: summoner001 <summoner@vivaldi.net>
Co-authored-by: Ghost of Sparta <makesocialfoss32@keemail.me>
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: jonnysemon <jonnysemon@users.noreply.hosted.weblate.org>
Co-authored-by: Bezruchenko Simon <worcposj44@gmail.com>
Co-authored-by: Antonio Oliveira <antoniovini47@gmail.com>
Co-authored-by: Eraorahan <eraorahan@gmail.com>
Co-authored-by: Abdullah Koyuncu <wisewebworks@outlook.com>
2024-10-08 18:27:23 +01:00
Stanislav Dmitrenko 54b40a5838 android, desktop: checking for camera in calls and handle uninitialized call (#4997)
* android, desktop: checking for camera in calls and handle uninitialized call

* explanation for situation without permission

* reorder

* reorder

* strings

* font

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-10-08 17:16:34 +01:00
Stanislav Dmitrenko 35fc0544a0 ui: do not enable speaker in calls when contact turned on video (#4998) 2024-10-08 15:57:33 +01:00
Evgeny 6907f02ea6 android, desktop: additional options for transport isolation mode (#4994)
* android, desktop: additional options for transport isolation mode

* small changes

---------

Co-authored-by: Avently <7953703+avently@users.noreply.github.com>
2024-10-08 14:36:08 +01:00
spaced4ndy c6ece8afdd ios: update library 2024-10-08 17:13:17 +04:00
spaced4ndy a00e5901de core: 6.1.0.7 2024-10-08 16:42:36 +04:00
spaced4ndy ad2a0024f2 core: add missing indexes (#4993) 2024-10-08 15:26:16 +04:00
Stanislav Dmitrenko c5261a416f ios: calls switching from audio to video and back (#4964)
* ios: switch calls

* working audio/video calls without screen recording

* ui

* follow up

* audio devices & permissions

* padding

* backward compatibility

* cleanup & fix

* buttons foreground color and converting call to video call from CallKit

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-10-08 08:49:13 +01:00
Stanislav Dmitrenko 193e17f7af android, desktop: thread-safe terminal items and floating terminal improvements (#4992)
* android, desktop: thread-safe terminal items and floating terminal improvements

* optimization
2024-10-08 08:08:22 +01:00
Evgeny Poberezkin 87fd642951 core: 6.1.0.6 2024-10-07 23:31:52 +01:00
Evgeny fb044000d2 cli: option to use web port 443 with SMP servers when port is not specified (#4942)
* cli: option to use web port 443 with SMP servers when port is not specified

* ui types

* remove imports
2024-10-07 23:30:52 +01:00
Arturs Krumins 69791dbdcf ios: fix appearance settings scroll hang (#4967)
* ios: fix appearance settings scroll hang

* simplify

* Revert "simplify"

This reverts commit f7b0aa74a4.

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-10-07 20:29:46 +01:00
Arturs Krumins dc11202250 ios: fix animator crash (#4990) 2024-10-07 20:22:06 +01:00
Evgeny 96ac0a7715 core: update simplexmq (transport block encryption) (#4881) 2024-10-07 18:39:31 +01:00
Arturs Krumins bdb6bd6e20 ios: hide user picker sheet instantly, when opening another sheet (#4927)
* ios: hide user picker sheet instantly, when opening another sheet

* tweak appearance

* distance based animation duration

* cleanup; dismiss

* implement UIViewPropertyAnimator

* resolve warning

* user picker bottom padding

* reset user scroll position on dismiss; cleanup

* reduce dif

* delay user picker closing

* touchable list row; prevent tap gesture passtrough

* fix dark mode tap target; retain highlight; highlight in user scroller

* fix layout loop; add upper animation speed constraint

* refactor separators

* instantanious longPress; tweak animations

* cubic animation curve; dynamic backdrop opacity

* remove touchdown animation

* ios: user picker sheet concurent animation (#4955)

* ios: user picker sheet concurent animation

* bind showSettings; cleanup

* async qr code generation

* fix iOS15 sheet animation when presenting sheet multiple times

* async camera authorization in 'Use from desktop' sheet

* load sheet navigation titles before presenting (#4963)

* load sheet navigation titles before presenting

* list background during loading

* improve handling of repeated sheet presentation state changes

* fix keyboard related glitches

* ios: remove `showSettings` and `withNavigation` (#4980)

* remove showSettings

* pass dismiss action trough navigation links

* move auth to all sheets

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-10-07 18:30:17 +01:00
spaced4ndy 7ccd80bf23 core, ios: try to get next ntf message to get expected (#4962) 2024-10-07 19:35:38 +04:00
Arturs Krumins f0d6f15393 ios: prevent image encoding from blocking the UI (#4966)
* ios: prevent image encoding from blocking the UI

* let

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-10-05 20:11:57 +01:00
Evgeny 49c91283d8 Translated using Weblate (Portuguese (Brazil)) (#4959)
Currently translated at 99.2% (255 of 257 strings)

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

Co-authored-by: Antonio Oliveira <antoniovini47@gmail.com>
2024-10-05 19:56:24 +01:00
Evgeny 55de5b9ce9 ui: translations (#4956)
* Translated using Weblate (German)

Currently translated at 100.0% (2067 of 2067 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% (1824 of 1824 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 100.0% (2067 of 2067 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% (1780 of 1824 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% (2067 of 2067 strings)

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

* Translated using Weblate (Romanian)

Currently translated at 35.0% (724 of 2067 strings)

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

* Translated using Weblate (Hungarian)

Currently translated at 100.0% (1824 of 1824 strings)

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

* Translated using Weblate (Hungarian)

Currently translated at 100.0% (1824 of 1824 strings)

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

* Translated using Weblate (Vietnamese)

Currently translated at 40.4% (837 of 2067 strings)

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

* Translated using Weblate (Chinese (Simplified Han script))

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

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

* Translated using Weblate (Japanese)

Currently translated at 89.5% (1852 of 2067 strings)

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

* Translated using Weblate (Hungarian)

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

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

* Translated using Weblate (Vietnamese)

Currently translated at 41.4% (856 of 2067 strings)

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

* Translated using Weblate (Hungarian)

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

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (1824 of 1824 strings)

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

* Translated using Weblate (Hungarian)

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

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

* Translated using Weblate (Dutch)

Currently translated at 100.0% (2067 of 2067 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.9% (1823 of 1824 strings)

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

* Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (2067 of 2067 strings)

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

* Translated using Weblate (Hungarian)

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

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

* Translated using Weblate (Vietnamese)

Currently translated at 42.6% (881 of 2067 strings)

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

* Translated using Weblate (Italian)

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

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

* Translated using Weblate (Portuguese (Brazil))

Currently translated at 49.5% (903 of 1824 strings)

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

* Translated using Weblate (Arabic)

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

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

* Translated using Weblate (Arabic)

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

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

* Translated using Weblate (Hungarian)

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

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

* Translated using Weblate (Ukrainian)

Currently translated at 100.0% (2067 of 2067 strings)

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (2068 of 2068 strings)

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

* Translated using Weblate (Spanish)

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

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

* Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (2068 of 2068 strings)

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

* Translated using Weblate (Ukrainian)

Currently translated at 100.0% (2068 of 2068 strings)

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

* Translated using Weblate (Polish)

Currently translated at 98.9% (2046 of 2068 strings)

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

* Translated using Weblate (Hungarian)

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

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

* Translated using Weblate (Arabic)

Currently translated at 100.0% (2068 of 2068 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 100.0% (2068 of 2068 strings)

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

* Translated using Weblate (Hungarian)

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

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

* Translated using Weblate (Turkish)

Currently translated at 95.2% (1970 of 2068 strings)

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (2069 of 2069 strings)

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

* Translated using Weblate (Hungarian)

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

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

* process localizations

---------

Co-authored-by: mlanp <github@lang.xyz>
Co-authored-by: No name <CertainBot@users.noreply.hosted.weblate.org>
Co-authored-by: summoner001 <summoner@vivaldi.net>
Co-authored-by: Zorro <3zorro.1@gmail.com>
Co-authored-by: Ghost of Sparta <makesocialfoss32@keemail.me>
Co-authored-by: tuananh-ng <158744840+tuananh-ng@users.noreply.github.com>
Co-authored-by: 大王叫我来巡山 <hamburger2048@users.noreply.hosted.weblate.org>
Co-authored-by: Miyu Sakatsuki <miyu-sakatsuki@outlook.jp>
Co-authored-by: Random <random-r@users.noreply.hosted.weblate.org>
Co-authored-by: M1K4 <oomikaoo@gmail.com>
Co-authored-by: Sarah Camila Lima <sarahnxjlima@gmail.com>
Co-authored-by: jonnysemon <jonnysemon@users.noreply.hosted.weblate.org>
Co-authored-by: Bezruchenko Simon <worcposj44@gmail.com>
Co-authored-by: c0d3r3d99 <chiara.ru2012@gmail.com>
Co-authored-by: Tadeusz Magura-Witkowski <tadeuszmw@gmail.com>
Co-authored-by: Batuhan Aydoğan <batuhanaydogan06@hotmail.com>
2024-10-05 19:55:45 +01:00
Diogo 8727d3b91b core: add chat message tail and roundness settings (#4977)
* core: add chat message tail and roundness settings

* ios: import/export chat message tail and roundness settings (#4978)
2024-10-05 19:44:26 +01:00
Diogo bb2a6ec65d android, desktop: add chat message tail and roundness settings (#4958)
* android, desktop: add roundness setting to chat items

* add tail setting

* use shape for clip

* wip tails

* shape style

* show tail only on last msg in group

* roundings

* padding for direct chats

* groups padding

* space between messages in settings preview

* refactor group paddings

* simplify

* simplify

* RcvDeleted handling

* revert uncessary

* import

* always maintain tail position

* rename

* reactions should not move

* short emoji shouldn't have tail

* remove invisible tail for voice without text

* better usage of gutters

* simplify

* rename

* simplify reactions

* linter happy

* exclude moderated items from shape

* uncessary diff

* func position

* fix chat view align on font resize (with image)

* fix tails moving bubble on max width

* fix big group names sometimes changing position

* small refactor

* fix top left corner end position

* rename

* sticky steps

* revert whitespace changes

---------

Co-authored-by: Avently <7953703+avently@users.noreply.github.com>
Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-10-05 19:02:09 +01:00
Evgeny Poberezkin 6118fca000 readme: update group links 2024-10-01 08:45:13 +01:00
Evgeny Poberezkin 026a8022e0 6.1-beta.3: ios 241, android 244, desktop 70 2024-09-30 23:00:16 +01:00
Evgeny Poberezkin dc1106afad ios: update core library 2024-09-30 19:56:09 +01:00
Evgeny Poberezkin cc9b4f3bb3 core: 6.1.0.5 (simplexmq: 6.1.0.3) 2024-09-30 18:29:20 +01:00
Diogo 533d0e40ac android, desktop: add floating date separator to chatview (#4951)
* android, desktop: add floating date separator to chatview

* closer near bottom

* uncessary code

* same pill bg as other btns

* space

* varname

* safe get for lastVisibleItem

* move floating date outside of floating buttons

* fast cleanup on chat change

* reduced recomposes

* change delay position

* base near bottom offset on viewport size

* refactor

* Revert "change delay position"

This reverts commit 27b19580ed.

* simplified

* exact match on header position

* reduce recomposes

---------

Co-authored-by: Avently <7953703+avently@users.noreply.github.com>
2024-09-30 15:45:32 +01:00
Stanislav Dmitrenko d9ad755474 android, desktop: make audio call default type of call on desktop (#4954)
* android, desktop: make audio call default type of call on desktop

* change
2024-09-30 15:45:13 +01:00
Stanislav Dmitrenko 15ca662805 android, desktop: support old Android WebViews (up to 69) (#4953)
* android, desktop: support old Android WebViews (up to 69)

* refactor

* WebView 70

* comment
2024-09-30 15:44:35 +01:00
spaced4ndy ab034e626f core: update simplexmq (#4952) 2024-09-30 14:53:36 +04:00
Diogo fc83bc692a android, desktop: make space on chat bubble end consistent (#4946)
* android, desktop: make space on chat bubble end consistent

* use non breaking spaces for reserve space

* avoid first white space non breaking to not drag content down
2024-09-28 18:26:43 +01:00
Evgeny Poberezkin d20d444e6e readme: update group links 2024-09-27 22:29:23 +01:00
Stanislav Dmitrenko fc0879ebb7 android, desktop: fix Safari sound (#4947)
* android, desktop: fix Safari sound

* another approach

* test

* Revert "test"

This reverts commit f89a30a88e.

* Revert "another approach"

This reverts commit 824ab7047c.

* Revert "android, desktop: fix Safari sound"

This reverts commit 80a866d472.

* android, desktop: fix Safari sound

* dependencies
2024-09-27 22:04:16 +01:00
Evgeny Poberezkin f048ddb922 6.1-beta.2: ios 240, android 243, desktop 69 2024-09-26 21:56:04 +01:00
Evgeny Poberezkin 0e39a62ab1 ios: update core library 2024-09-26 21:03:41 +01:00
Diogo 53f0fe9ca4 android, desktop: time based message grouping and day separators (#4914)
* android, desktop: message grouping

* short format on chat

* separator for dates

* simplify

* show on separator when not current year

* default for showing date on markdown text

* remove unused code

* refactor

* refactor

* remove default locally

* fixed build

* fix

* show first date in chat

* apply padding to selectable area

* fix date on chats for previous days

* add year formatting

* fixed message grouping and time show

* remove log

* fixed reserved space for meta

* align first chat bubble with image

* metadata correct space

* remove log

* simplify item separation logic

* cleanuo

* icon tweaks

* without unneeded element

* match ios logic

* CIMetaText fix

* split selectable area

* Revert "split selectable area"

This reverts commit 1c6001ba3d.

* reserve space similar to ios

* split spacing for chat item selection

* less repeated code

* format

* increase padding

---------

Co-authored-by: Avently <7953703+avently@users.noreply.github.com>
Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-09-26 20:26:33 +01:00
Stanislav Dmitrenko 95c1d8d798 android, desktop: calls switching from audio to video and back (#4814)
* android, desktop: calls switching from audio to video and back

* refactor

* working all 4 streams with mute handling differently

* changes

* changes

* wrong file

* changes

* padding

* android camera service type

* icons, sizes, clickable

* refactor

* Revert "android camera service type"

This reverts commit 9878ff38e9.

* late init camera permissions

* enabling camera sooner than call establishes (not fully done)

* changes

* alpha

* fixes for Safari

* enhancements

* fix Safari sound

* padding between buttons on desktop

* android default values for padding

* changes

* calls without encryption are supported and flipping camera on some devices works

* unused param

* logs

* background color

* play local video in Safari

* no line height

* removed one listener from per frame processing

* enhancements

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-09-26 20:18:05 +01:00
Evgeny 4a39b481b1 ios: avoid message changing width when sent/received ticks appear (#4945) 2024-09-26 17:28:14 +01:00
Evgeny Poberezkin 65c7ecbddf core: 6.1.0.4 (simplexmq 6.1.0.1) 2024-09-26 13:45:12 +01:00
Diogo 67472b6285 android, desktop: scrolling user profiles (#4939)
* android, desktop: scrolling user profiles

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
Co-authored-by: Avently <7953703+avently@users.noreply.github.com>
2024-09-26 09:00:10 +01:00
Arturs Krumins 9199fbffd5 ios: fix add members search keyboard focus (#4934)
* ios: fix add members search keyboard focus

* use -1 as ID

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-09-25 22:26:04 +01:00
spaced4ndy 6e5eb697a2 core: use broker ts for member profile update item ts (#4940) 2024-09-25 18:33:20 +01:00
spaced4ndy e2e6935e5b core: fix reactions not being read on item updates (#4938) 2024-09-25 11:16:32 +01:00
Evgeny Poberezkin 769ef25c31 Merge branch 'stable' 2024-09-24 23:01:20 +01:00
Evgeny Poberezkin 2f730d54e9 6.0.5: ios 239, android 241, desktop 68 2024-09-24 21:48:30 +01:00
Evgeny Poberezkin fe0013c4a9 ios: update core library 2024-09-24 17:51:34 +01:00
Stanislav Dmitrenko 5261886b31 android, desktop: proxy configuration includes credentials (#4892)
* android, desktop: proxy configuration includes credentials

* migration

* changes for disabled socks

* migration

* port

* new logic

* migration

* check validity of fields

* validity of host

* import changes proxy just in case

* send port always

* non-nullable

* Revert "send port always"

This reverts commit 14dd066d80.

* string

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-09-24 17:07:39 +01:00
Evgeny 93ab3076d4 ios: SOCKS proxy UI (#4893)
* ios: SOCKS proxy UI

* update network config

* proxy

* adapt

* move, dont default to localhost:9050

* move socks proxy to defaults

* sock proxy preference

* rename

* rename

* fix

* fix

---------

Co-authored-by: Avently <7953703+avently@users.noreply.github.com>
2024-09-24 17:06:00 +01:00
Stanislav Dmitrenko 3b88ddbd4f desktop: fix vlc dependency (2) (#4869) 2024-09-24 17:04:08 +01:00
spaced4ndy 54ff95f350 ios: fix theme customization changing color mode (#4936) 2024-09-24 15:44:55 +01:00
Evgeny Poberezkin cd65e4dbb9 Merge branch 'stable' into stable-android 2024-09-24 12:42:55 +01:00
Evgeny Poberezkin d6dc35738e core: 6.0.5.0 (simplexmq 6.0.5.0) 2024-09-24 12:42:22 +01:00
Evgeny 5b3aba9db2 ci: dont build when files in core do not change (#4797) 2024-09-24 12:40:58 +01:00
Diogo 4526afe7e9 desktop: wrap content of remote hosts on overflow (#4923)
* desktop: wrap content of remote hosts on overflow

* fix long device text and align

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-09-24 11:51:02 +01:00
Evgeny Poberezkin c67302a5bb core: update simplexmq 2024-09-24 09:51:21 +01:00
Evgeny Poberezkin 3685c85743 ios: reduce scroll stickiness threshold for user profiles to 32px 2024-09-24 09:46:54 +01:00
Evgeny 0f301adc57 core: xrcp encryption with forward secrecy (#4926)
* core: xrcp encryption with forward secrecy (tests intermittently fail)

* track and correlate keys

* simplify

* refactor

* remove comment
2024-09-24 09:25:41 +01:00
Stanislav Dmitrenko 1f226dda64 android: fix status bar color after hiding call (#4928)
* android: fix status bar color after hiding call

* dark status bar in call
2024-09-24 09:25:14 +01:00
Stanislav Dmitrenko 981cbb8bf9 android: target API level 34 (Android 14) (#4697) 2024-09-23 09:05:14 +01:00
Stanislav Dmitrenko d5507f2fa3 android, desktop: member name position depends on length (#4918)
* android, desktop: member name position depends on length

* maxWidth limit

* fix

* optimization

* paddings

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-09-23 08:57:59 +01:00
Narasimha-sc 55d180466a docs: iOS notifications in FAQ (#4879)
* docs: iOS notifications in FAQ

* Update FAQ.md
2024-09-21 21:34:13 +01:00
Arturs Krumins 8a906485d1 ios: display year in chat for previous years (#4919)
* ios: display year in chat for previous years

* fix chat time, show past years in the list

* style

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-09-21 21:33:18 +01:00
Arturs Krumins e79fa136a4 ios: fix keyboard loosing focus when forward search results are empty (#4895) 2024-09-21 19:26:42 +01:00
Evgeny 560b521673 ios: scrolling user profiles (#4909)
* ios: scrolling user profiles

---------

Co-authored-by: Levitating Pineapple <noreply@levitatingpineapple.com>
2024-09-21 19:12:53 +01:00
Evgeny c849f5356d core: save the time user profile was opened at to order in ui (#4920)
* core: save the time user profile was opened at to order in ui

* replace timestamp with order
2024-09-21 13:07:27 +01:00
Evgeny Poberezkin 33e12e35a0 ios: use translation in dropdown 2024-09-20 21:19:22 +01:00
spaced4ndy 8a70bad9af core: process ERRS event (#4896)
* core: process ERRS event

* refactor

* update

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-09-20 21:13:27 +04:00
Evgeny Poberezkin efb7fc6c3b 6.1-beta.1: ios 238, android 240, desktop 67 2024-09-20 15:01:47 +01:00
Evgeny 630fea42c3 website: translations (#4916)
* ios: update core library

* Translated using Weblate (Hungarian)

Currently translated at 100.0% (257 of 257 strings)

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

* Revert "ios: update core library"

This reverts commit 211f6c51f2.

---------

Co-authored-by: summoner001 <summoner@vivaldi.net>
2024-09-20 12:47:40 +01:00
Evgeny 68e570656d ui: translations (#4915)
* ios: update core library

* Translated using Weblate (Portuguese)

Currently translated at 46.2% (944 of 2041 strings)

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

* Translated using Weblate (Hungarian)

Currently translated at 100.0% (2041 of 2041 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% (2041 of 2041 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.2% (1787 of 1800 strings)

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

* Translated using Weblate (Hungarian)

Currently translated at 99.2% (1787 of 1800 strings)

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (2041 of 2041 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% (1800 of 1800 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% (2041 of 2041 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.9% (1799 of 1800 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% (2041 of 2041 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% (1800 of 1800 strings)

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

* Translated using Weblate (Hungarian)

Currently translated at 100.0% (2041 of 2041 strings)

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

* Translated using Weblate (Ukrainian)

Currently translated at 100.0% (2041 of 2041 strings)

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

* Translated using Weblate (Hungarian)

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

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

* Translated using Weblate (Japanese)

Currently translated at 88.6% (1809 of 2041 strings)

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

* Translated using Weblate (Vietnamese)

Currently translated at 38.5% (786 of 2041 strings)

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

* Translated using Weblate (Vietnamese)

Currently translated at 38.8% (793 of 2041 strings)

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

* Translated using Weblate (German)

Currently translated at 99.9% (2040 of 2041 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% (2051 of 2051 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% (1800 of 1800 strings)

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

* Translated using Weblate (Chinese (Simplified Han script))

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

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (2051 of 2051 strings)

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

* Translated using Weblate (Japanese)

Currently translated at 90.0% (1847 of 2051 strings)

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

* Translated using Weblate (Hungarian)

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

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

* Translated using Weblate (Ukrainian)

Currently translated at 100.0% (2051 of 2051 strings)

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

* Translated using Weblate (Hungarian)

Currently translated at 100.0% (2051 of 2051 strings)

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

* Translated using Weblate (Vietnamese)

Currently translated at 39.4% (810 of 2051 strings)

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

* Translated using Weblate (Hungarian)

Currently translated at 100.0% (2051 of 2051 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% (2051 of 2051 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% (2067 of 2067 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% (1800 of 1800 strings)

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

* Translated using Weblate (Vietnamese)

Currently translated at 40.0% (827 of 2067 strings)

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (2067 of 2067 strings)

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

* Translated using Weblate (Arabic)

Currently translated at 99.3% (2054 of 2067 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% (2067 of 2067 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% (1800 of 1800 strings)

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

* ios: update core library

* Translated using Weblate (Portuguese)

Currently translated at 46.2% (944 of 2041 strings)

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

* Translated using Weblate (Hungarian)

Currently translated at 100.0% (2041 of 2041 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% (2041 of 2041 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.2% (1787 of 1800 strings)

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

* Translated using Weblate (Hungarian)

Currently translated at 99.2% (1787 of 1800 strings)

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (2041 of 2041 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% (1800 of 1800 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% (2041 of 2041 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.9% (1799 of 1800 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% (2041 of 2041 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% (1800 of 1800 strings)

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

* Translated using Weblate (Hungarian)

Currently translated at 100.0% (2041 of 2041 strings)

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

* Translated using Weblate (Ukrainian)

Currently translated at 100.0% (2041 of 2041 strings)

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

* Translated using Weblate (Hungarian)

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

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

* Translated using Weblate (Japanese)

Currently translated at 88.6% (1809 of 2041 strings)

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

* Translated using Weblate (Vietnamese)

Currently translated at 38.5% (786 of 2041 strings)

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

* Translated using Weblate (Vietnamese)

Currently translated at 38.8% (793 of 2041 strings)

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

* Translated using Weblate (German)

Currently translated at 99.9% (2040 of 2041 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% (2051 of 2051 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% (1800 of 1800 strings)

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

* Translated using Weblate (Chinese (Simplified Han script))

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

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (2051 of 2051 strings)

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

* Translated using Weblate (Japanese)

Currently translated at 90.0% (1847 of 2051 strings)

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

* Translated using Weblate (Hungarian)

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

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

* Translated using Weblate (Ukrainian)

Currently translated at 100.0% (2051 of 2051 strings)

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

* Translated using Weblate (Hungarian)

Currently translated at 100.0% (2051 of 2051 strings)

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

* Translated using Weblate (Vietnamese)

Currently translated at 39.4% (810 of 2051 strings)

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

* Translated using Weblate (Hungarian)

Currently translated at 100.0% (2051 of 2051 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% (2051 of 2051 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% (2067 of 2067 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% (1800 of 1800 strings)

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

* Translated using Weblate (Vietnamese)

Currently translated at 40.0% (827 of 2067 strings)

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (2067 of 2067 strings)

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

* Translated using Weblate (Arabic)

Currently translated at 99.3% (2054 of 2067 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% (2067 of 2067 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% (1800 of 1800 strings)

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

* process localizations

* fix interpolation

---------

Co-authored-by: Antonio Oliveira <antoniovini47@gmail.com>
Co-authored-by: summoner001 <summoner@vivaldi.net>
Co-authored-by: Ghost of Sparta <makesocialfoss32@keemail.me>
Co-authored-by: Random <random-r@users.noreply.hosted.weblate.org>
Co-authored-by: M1K4 <oomikaoo@gmail.com>
Co-authored-by: Bezruchenko Simon <worcposj44@gmail.com>
Co-authored-by: Miyu Sakatsuki <miyu-sakatsuki@outlook.jp>
Co-authored-by: tuananh-ng <158744840+tuananh-ng@users.noreply.github.com>
Co-authored-by: mlanp <github@lang.xyz>
Co-authored-by: 大王叫我来巡山 <hamburger2048@users.noreply.hosted.weblate.org>
Co-authored-by: jonnysemon <jonnysemon@users.noreply.hosted.weblate.org>
2024-09-20 12:34:38 +01:00
Evgeny Poberezkin 3cbb2c2d71 core: 6.1.0.3 2024-09-20 09:45:30 +01:00
spaced4ndy 5ca27f63e6 core: send errors processing (#4910)
* core: send errors processing

* test
2024-09-20 09:27:14 +01:00
Evgeny Poberezkin 63d1b2060e Merge branch 'stable' 2024-09-19 18:01:51 +01:00
Stanislav Dmitrenko 25fb099c44 android, desktop: fix always connecting state in call (#4911) 2024-09-19 18:00:08 +01:00
Diogo d69e222b7b android, desktop: bulk forward (#4868)
---------

Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-09-19 15:39:14 +01:00
Evgeny Poberezkin f7cb6f2796 Revert "android, desktop: compose 1.6.11 (#4902)"
This reverts commit acc9be1a5b.
2024-09-19 14:10:07 +01:00
Stanislav Dmitrenko c5813b3489 android, desktop: catch URI creation errors (#4901)
* android, desktop: catch URI creation errors

* showing alert when pasting an incorrect link

* moved from Uri to String while processing SimpleX links
2024-09-19 09:36:54 +01:00
spaced4ndy 10ded1530c desktop: cleanup current invitation when closing new chat sheet by pressing on center modal (#4904) 2024-09-19 08:14:50 +01:00
Arturs Krumins 255538e5d7 ios: bulk forward (#4857)
* ios: forward multiple messages

* ios: batch previews, when sending media messsages (#4861)

---------

Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
Co-authored-by: Arturs Krumins <auth@levitatingpineapple.com>
Co-authored-by: Diogo <diogofncunha@gmail.com>
Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-09-19 08:04:19 +01:00
Stanislav Dmitrenko acc9be1a5b android, desktop: compose 1.6.11 (#4902) 2024-09-19 07:57:37 +01:00
Diogo d47ff3597d android, desktop: reset incognito on profile picker (#4903) 2024-09-18 19:16:32 +04:00
Evgeny Poberezkin 529921e16a 6.1-beta.0: ios 237, android 239, desktop 66 2024-09-18 11:37:32 +01:00
Evgeny Poberezkin 0a4667304c ios: update core library 2024-09-18 10:18:06 +01:00
Evgeny Poberezkin 17a0f3a210 core: 6.1.0.2, update min versions for remote access to 6.1.0.2 2024-09-18 08:56:43 +01:00
Evgeny Poberezkin c13e8e4037 ios: update core library 2024-09-18 07:28:06 +01:00
Evgeny Poberezkin 3a61290730 core: 6.1.0.1 2024-09-17 22:30:24 +01:00
Evgeny Poberezkin 166082c021 Merge branch 'stable' 2024-09-17 22:23:58 +01:00
Stanislav Dmitrenko 69bbe0ae91 android, desktop: proxy configuration includes credentials (#4892)
* android, desktop: proxy configuration includes credentials

* migration

* changes for disabled socks

* migration

* port

* new logic

* migration

* check validity of fields

* validity of host

* import changes proxy just in case

* send port always

* non-nullable

* Revert "send port always"

This reverts commit 14dd066d80.

* string

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-09-17 21:09:50 +01:00
spaced4ndy 0d9ef4e567 ui: support CRChatItemsStatusesUpdated response (#4890)
* ui: support new CRChatItemsStatusesUpdated api

* ios

* refactor
2024-09-17 20:51:18 +01:00
spaced4ndy 17b55c51c5 core: update statuses of all batched messages on SENT, RCVD (#4888)
* core: update statuses of all batched messages on SENT, RCVD

* wip

* update all

* refactor
2024-09-17 20:50:26 +01:00
Evgeny 665d9dcd00 ios: SOCKS proxy UI (#4893)
* ios: SOCKS proxy UI

* update network config

* proxy

* adapt

* move, dont default to localhost:9050

* move socks proxy to defaults

* sock proxy preference

* rename

* rename

* fix

* fix

---------

Co-authored-by: Avently <7953703+avently@users.noreply.github.com>
2024-09-17 17:34:24 +01:00
Stanislav Dmitrenko c13c7baaaf desktop: less distance from edge to call icon (#4898) 2024-09-17 17:07:29 +01:00
Evgeny af993529f9 core: migrate SOCKS proxy settings (#4894) 2024-09-17 12:23:50 +01:00
Evgeny Poberezkin 05aab35a1f ios: update core library 2024-09-16 18:23:59 +01:00
Evgeny 40e93cc61e core: reduce max message sizes (#4882)
* core: reduce max message sizes

* reduce

* comment
2024-09-16 18:05:09 +01:00
Evgeny ea320f531f Translated using Weblate (Spanish) (#4891)
Currently translated at 100.0% (257 of 257 strings)

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

Co-authored-by: Eraorahan <eraorahan@gmail.com>
2024-09-16 16:53:11 +01:00
Evgeny 4f99075b14 ui: translations (#4889)
* Translated using Weblate (Hungarian)

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

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

* Translated using Weblate (Vietnamese)

Currently translated at 36.4% (741 of 2032 strings)

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

* Translated using Weblate (Hungarian)

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

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

* Translated using Weblate (Indonesian)

Currently translated at 5.8% (118 of 2032 strings)

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

* Translated using Weblate (Hungarian)

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

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

* Translated using Weblate (Vietnamese)

Currently translated at 36.4% (741 of 2032 strings)

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

* Translated using Weblate (Hungarian)

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

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

* Translated using Weblate (Indonesian)

Currently translated at 5.8% (118 of 2032 strings)

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

* Translated using Weblate (Hungarian)

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

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

* Translated using Weblate (German)

Currently translated at 100.0% (2032 of 2032 strings)

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

* Translated using Weblate (Indonesian)

Currently translated at 6.3% (130 of 2032 strings)

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

* Translated using Weblate (German)

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

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (1785 of 1785 strings)

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

* Translated using Weblate (Portuguese (Brazil))

Currently translated at 83.5% (1697 of 2032 strings)

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

* Translated using Weblate (Indonesian)

Currently translated at 12.9% (263 of 2032 strings)

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

* Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (2032 of 2032 strings)

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

* Translated using Weblate (Hungarian)

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

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

* Translated using Weblate (Vietnamese)

Currently translated at 37.1% (755 of 2032 strings)

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

* Translated using Weblate (Vietnamese)

Currently translated at 37.8% (769 of 2032 strings)

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

* Translated using Weblate (Czech)

Currently translated at 88.7% (1804 of 2032 strings)

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

* Translated using Weblate (Vietnamese)

Currently translated at 38.4% (782 of 2032 strings)

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

* process localizations

* fix links

---------

Co-authored-by: summoner001 <summoner@vivaldi.net>
Co-authored-by: tuananh-ng <158744840+tuananh-ng@users.noreply.github.com>
Co-authored-by: rbasliana <rbasliana@protonmail.com>
Co-authored-by: Ghost of Sparta <makesocialfoss32@keemail.me>
Co-authored-by: mlanp <github@lang.xyz>
Co-authored-by: re me <jisizhang001@gmail.com>
Co-authored-by: Mateus Pereira <desktecc.union091@passinbox.com>
Co-authored-by: rbasliana <91536894+rbasliana@users.noreply.github.com>
Co-authored-by: zenobit <zen@osowoso.xyz>
2024-09-16 16:50:37 +01:00
Arturs Krumins de7882c904 ios: update user profile sheet design (#4871)
* ios: update user profile sheet design

* revert views

* improve validation

* minor

* align with create profile

* alert on dismiss

* revert x appearance

* update size

* move the fullname

* focus on appear

* profile image

* localizations

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-09-16 13:28:45 +01:00
Evgeny Poberezkin 0cad378bc5 core: 6.1.0.0 (simplexmq 6.1.0.0) 2024-09-16 13:17:18 +01:00
Evgeny Poberezkin da058ca376 Merge branch 'stable' 2024-09-16 08:09:53 +01:00
Evgeny c22d23750f core: support different SOCKS proxy authentication modes (#4886)
* core: support different SOCKS proxy authentication modes

* use defaultSocksProxyWithAuth

* hostMode CLI option

* simplexmq
2024-09-16 07:33:48 +01:00
Evgeny b5f91f2d31 core: test forwarding image preview without attached file (#4877) 2024-09-14 10:57:55 +01:00
spaced4ndy c6ab8ec6b3 core: cleanup empty file on error; check file status on forward (#4878) 2024-09-13 23:16:23 +04:00
Stanislav Dmitrenko 4447b66b4e android: fix showing logs from core (#4880) 2024-09-13 20:02:32 +01:00
Stanislav Dmitrenko 2539255957 android: remove Worker provider's interface implementation (#4874) 2024-09-13 12:35:12 +01:00
Evgeny 4cfda91124 core: fix ForwardConfirmation encoding (#4872) 2024-09-13 09:35:11 +01:00
Evgeny Poberezkin 7aec147cec core: update simplexmq 6.0.4.0 2024-09-12 22:19:17 +01:00
Evgeny f6f2044675 core: plan forwarding chat items, api types (#4865)
* core: plan forwarding chat items, api types

* remove empty content, refactor get items

* another refactor

* plan

* test

* more tests

* text
2024-09-12 15:21:29 +01:00
Stanislav Dmitrenko 5f0b5c5a9f desktop: fix vlc dependency (2) (#4869) 2024-09-12 14:51:57 +01:00
Evgeny Poberezkin dfdb4af646 Revert "core: bulk forward missing files error handling (#4860)"
This reverts commit 46d774a822.
2024-09-12 08:52:09 +01:00
Narasimha-sc 2ab5f14119 docs: remove outdated latest version number from downloads (#4854) 2024-09-11 21:31:05 +01:00
Diogo 46d774a822 core: bulk forward missing files error handling (#4860)
* add types

* wip dump

* collect errors

* Update src/Simplex/Chat/View.hs

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

* test with not received files

* remove ciFileLoaded

* undo refactoring

* test for skipping missing file with text

* add test for empty message

* remove fdescribes

* copy or cleanup files after collecting errors and forward reqs

* don't forward w/t content

* translate CIFSRcvAborted into FFENotAccepted

* refactor

* refactor

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
2024-09-11 21:30:09 +01:00
Diogo acf2f1fbbe android, desktop: new user picker (#4796)
* user picker as modal

* android dirty layout

* color mode switcher

* close picker on desktop opening modals

* cleanup

* remote hosts working

* icon buttons

* profile picker modal for external shares

* remove stroke

* color changes

* add unread badge to users row

* chat database settings section

* safe remove of you settings section

* picker should now open for single user

* remove create profile from settings

* paddings

* handle big names

* fonts and align

* simple animations and shadow

* address messaging

* active is grey

* padding

* hide non active devices from pills

* picker positioning

* pills order

* change view all profiles icon

* bigger space between profiles

* hosts ordering and fixes

* device pill in app bar

* simplex address -> public

* better switch of opacity bg

* create public address

* font match

* add icon for dark mode

* padding

* profile name as header

* h2 is too big

* icon colors

* icons

* settings as modal

* center settings

* fix use from desktop

* remove logs

* bar colors

* remove drawer unused code

* animate shading

* fade in fade out

* add system mode toast

* shading colors

* stop pushing shade up

* same button as ios for opening all profiles

* simplify nav bar color set

* broken transition change

* color mix

* gradient and horizontal scroll

* separate title

* align avatars to top

* picker should always remain open

* use chevron icon to see all profiles

* improvements on status and nav color set

* best case bars switching working

* change bar and shading on theme change

* remove unused var

* reset navbar colors on navigate

* updated icon color

* protect android calls

* desktop menu matching size of right side modals

* remove shading from desktop

* close user picker on settings click in desktop

* bigger profile image smaller gap to name

* fix spacer for row scroll on android

* smaller profile name

* remove unused code

* small refactor

* unused

* move desktop/mobile connection

* close drawer on swipe down 30%

* progress dump on new android design

* paddings in scroller

* gradient

* android paddings

* split inactive user picker between platforms

* move your chat profiles inside android specific

* always show your chat profiles in desktop

* fix profile creation in desktop

* remove unused var

* update android space between badges

* initial desktop design

* center android icons with avatar

* centered avatars

* unread badge

* extra space in the end of user list for android

* aligned paddings on desktop

* desktop paddings

* paddings

* remove you

* unread badge same style as chatlist

* use bedtime moon for dark mode

* chevron same size as sun/moon

* chevron and gradient

* paddings

* split android and desktop scaffold for picker

* move bars logic to android

* remove android check

* more android checks

* initial version of swipable modal

* muted as grey

* unused

* close drawer on 3/4

* better close control

* make all animations match

* move shadow with offset

* always close pciker on selection

* animated float doing nothing

* sync animation

* animation using single float

* fixed warnings

* better state update

* fix scrim color

* better handling of picker closure on desktop

* landscape mode

* intentation

* rename UserPickerScaffold

* hide shadow when picker not open

* reset inactive user scroll position on pick

* unused class

* left panel after new menu can be without padding

* small changes

* make ActiveProfilePicker reusable to reduce code duplication

* make picker scrollable

* refactor

* refactor and fix instant reload of profiles

* refactor

* icon sizes

* returned back ability to scroll to the picker on Android

* setting system theme on desktop's right click

* box

* refactor

* picker pill

* fix desktop shadow

* small change

* hiding keyboard when opening picker

* state specifying

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
Co-authored-by: Avently <7953703+avently@users.noreply.github.com>
2024-09-11 15:51:28 +01:00
Evgeny Poberezkin 1a853d4eea Merge branch 'stable' 2024-09-11 13:06:11 +01:00
spaced4ndy 3ea8279451 multiplatform: fix delete messages alert (#4862) 2024-09-11 13:04:59 +01:00
spaced4ndy 87c55dbbf7 multiplatform: fix delete messages alert (#4862) 2024-09-11 15:08:16 +04:00
Evgeny fb4475027d ios: new user picker (#4821)
* ios: new user picker (#4770)

* current user picker progress

* one hand picker

* thin bullet icon

* more user picker buttons

* button clickable areas

* divider padding

* extra space after sun

* send current user option to address view

* add unread count badge

* with anim for apperance close

* edit current profile from picker

* remove you section from settings

* remove help and support

* simplify

* move settings and sun to same row

* remove redundant vstack

* long press on sun/moon switches to system setting

* remove back button from migrate device

* smooth profile transitions

* close user picker on list profiles

* fix dismiss on migrate from device

* fix dismiss when deleting last visible user while having hidden users

* picker visibility toggle tweaks

* remove strange square from profile switcher click

* dirty way to save auto accept settings on dismiss

* Revert "dirty way to save auto accept settings on dismiss"

This reverts commit e7b19ee8aa.

* consistent animation on user picker toggle

* change space between profiles

* remove result

* ignore result

* unread badge

* move to sheet

* half sheet on one hand ui

* fix dismiss on device migration

* fix desktop connect

* sun to meet other action icons

* fill bullet list button

* fix tap in settings to take full width

* icon sizings and paddings

* open settings in same sheet

* apply same trick as other buttons for ligth toggle

* layout

* open profiles sheet large when +3 users

* layout

* layout

* paddings

* paddings

* remove show progress

* always small user picker

* fixed height

* open all actions as sheets

* type, color

* simpler and more effective way of avoid moving around on user select

* dismiss user profiles sheet on user change

* connect desktop back button remove

* remove back buttons from user address view

* remove porgress

* header inside list

* alert on auto accept unsaved changes

* Cancel -> Discard

* revert

* fix connect to desktop

* remove extra space

* fix share inside multi sheet

* user picker and options as separate sheet

* revert showShareSheet

* fix current profile and all profiles selection

* change alert

* update

* cleanup user address

* remove func

* alert on unsaved changes in chat prefs

* fix layout

* cleanup

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>

* ios: fix switching profiles (#4822)

* ios: different user picker layout (#4826)

* ios: different user picker layout

* remove section

* layout, color

* color

* remove activeUser

* fix gradient

* recursive sheets

* gradient padding

* share sheet

* layout

* dismiss sheets

---------

Co-authored-by: Levitating Pineapple <noreply@levitatingpineapple.com>

* ios: use the same way to share from all sheets (#4829)

* ios: close user picker before opening other sheets

* Revert "share sheet"

This reverts commit 0064155825.

* dismiss/show via callback

* Revert "ios: close user picker before opening other sheets"

This reverts commit 19110398f8.

* ios: show alerts from sheets (#4839)

* padding

* remove gradient

* cleanup

* simplify settings

* padding

---------

Co-authored-by: Diogo <diogofncunha@gmail.com>
Co-authored-by: Levitating Pineapple <noreply@levitatingpineapple.com>
Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
2024-09-10 09:31:53 +01:00
spaced4ndy 388609563d core: update simplexmq (ntf encoding) (#4853) 2024-09-09 18:22:14 +04:00
Evgeny 0cb568d206 fix incorrect error of migration to device (#4852)
* fix incorrect error of migration to device

* alert to finish migration, ios fix

* simplexmq

* catching exception and stopping chat

* text

---------

Co-authored-by: Avently <7953703+avently@users.noreply.github.com>
2024-09-09 15:01:18 +01:00
Arturs Krumins 8f6e9741e7 ios: add floating date separator (#4801)
* ios: add floating date separator

* floating date separator

* revert formatTimestampText

* send tuple, reduce lookups

* background date visibility

* whitespace

* streamline

* visible date

* move pipeline to ReverseList

* space

* remove ViewUpdate

* cleanup

* refactor

* combine unread items model updates

* split publisher

* remove readItemPublisher

* revert markChatItemRead_ change

* use single item api

* comment test buttons

* style

* update top floating button instantly

* cleanup

* cleanup

* minor

* remove task

* prevent concurrent updates

* fix mark chat read

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-09-09 14:58:22 +01:00
Evgeny Poberezkin e00001b571 Merge branch 'stable' 2024-09-08 23:01:54 +01:00
Evgeny 351cfcbcbc core: allow deleting user when user record in agent database was deleted (#4851) 2024-09-08 23:01:31 +01:00
Evgeny 74b837bf9a core: allow deleting user when user record in agent database was deleted (#4851) 2024-09-08 20:02:38 +01:00
Henrique Albuquerque 691cd489ea blog: fix grammar error (#4846) 2024-09-08 12:56:15 +01:00
Evgeny 5ed701402b core: optimize marking chat items as read, batch API (#4843)
* core: optimize marking chat items as read

* tests, ui types

* ios: fix api

* refactor
2024-09-07 19:40:10 +01:00
Evgeny 1839dab17b ios: move caching images to background thread, dont use main thread scheduler for marking items read (#4840) 2024-09-06 22:09:55 +01:00
Arturs Krumins 06939343a1 ios: revert showing date in chat list timestamp (#4834) 2024-09-06 13:32:41 +01:00
Evgeny Poberezkin b2ebb81fcf Merge branch 'stable' 2024-09-06 12:40:16 +01:00
Stanislav Dmitrenko bccbb9900f android: fix initializing WorkManager (#4833) 2024-09-06 12:39:23 +01:00
Arturs Krumins 1f3355921c ios: make message information on media readable (#4820)
* ios: ensure legibility of elements rendered over media

* reduce diff

* match meta padding

* material play background

* remove circlebadge

* progress circle

* meta color modes

* refactor

* conditional space

* fix

* fix2

* fix3

* revert video buttons

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-09-06 12:36:54 +01:00
Evgeny Poberezkin 9e45f68a6e Merge branch 'stable' 2024-09-05 12:06:11 +01:00
Evgeny ebb5d629e7 ios: attach share sheet to the topmost view controller (to support sharing from nested sheets) (#4830) 2024-09-04 23:12:05 +01:00
Diogo e002f33c53 multiplatform: fix deleted contacts with conversations being identified as contact cards (#4828)
* multiplatform: fix deleted contacts with conversations being identified as contact cards

* fix in ios

* Revert "fix in ios"

This reverts commit 9b8c6bc125.

* fix for ios

* same check as ios for android and desktop
2024-09-04 18:12:41 +01:00
Evgeny 71bea947a5 ios: cache base64 images (#4827) 2024-09-04 14:49:01 +01:00
Evgeny Poberezkin 90d5abdff1 Merge branch 'stable' 2024-09-03 14:39:34 +01:00
Stanislav Dmitrenko 6407d5de63 android, desktop: making list items unique (#4812) 2024-09-03 14:38:42 +01:00
Stanislav Dmitrenko 9cba96082d android, desktop: fix crash when window is small in chat view (#4819) 2024-09-03 14:37:04 +01:00
Stanislav Dmitrenko 2089fd8539 android, desktop: remove slash from strings (#4818) 2024-09-03 14:36:41 +01:00
Stanislav Dmitrenko 31c4ff2705 android, desktop: group info remembers scroll position and search (#4817) 2024-09-03 14:36:01 +01:00
sh fe20a43232 flatpak: update metainfo (#4811) 2024-09-02 15:15:16 +01:00
Stanislav Dmitrenko 087519c39d desktop: fix vlc dependency (#4809) 2024-09-02 13:08:09 +01:00
Evgeny Poberezkin f5d5bd62ed Merge branch 'stable' into stable-android 2024-08-30 20:00:44 +01:00
Evgeny Poberezkin d2f9888195 Merge branch 'stable' into stable-android 2024-08-30 13:40:03 +01:00
Evgeny Poberezkin 5ec5826642 Merge branch 'stable' into stable-android 2024-08-24 16:49:03 +01:00
Evgeny Poberezkin 29155ab73a Merge branch 'stable' into stable-android 2024-08-24 15:10:05 +01:00
Evgeny Poberezkin 31782600cb Merge branch 'stable' into stable-android 2024-06-01 09:28:13 +01:00
Evgeny Poberezkin db392037ea Merge branch 'stable' into stable-android 2024-05-31 23:35:46 +01:00
Evgeny Poberezkin 6ad7189c53 Merge branch 'stable' into stable-android 2024-05-25 17:17:18 +01:00
Evgeny Poberezkin 15d8444c74 Merge branch 'stable' into stable-android 2024-05-25 11:19:14 +01:00
Evgeny Poberezkin 2d31b853ac Merge branch 'stable' into stable-android 2024-05-22 20:30:46 +01:00
Evgeny Poberezkin 3af0108b0d Merge branch 'stable' into stable-android 2024-04-13 11:59:14 +01:00
Evgeny Poberezkin 746dd33728 Merge branch 'stable' into stable-android 2024-03-22 21:21:10 +00:00
Evgeny Poberezkin 2fceecab37 Merge branch 'stable' into stable-android 2024-03-04 21:38:09 +00:00
Evgeny Poberezkin e094ec5cf5 Merge branch 'stable' into stable-android 2024-02-27 07:19:21 +00:00
Evgeny Poberezkin fbd6012611 Merge branch 'stable' into stable-android 2024-02-21 19:06:13 +00:00
Evgeny Poberezkin 913ce4ebfd Merge branch 'stable' into stable-android 2024-02-21 14:30:35 +00:00
Evgeny Poberezkin 1e01a961fe Merge branch 'stable' into stable-android 2024-02-21 08:43:40 +00:00
Evgeny Poberezkin 27f4a71146 Merge branch 'stable' into stable-android 2024-02-10 23:35:40 +00:00
Evgeny Poberezkin 282bca4d81 Merge branch 'stable' into stable-android 2024-02-03 18:45:28 +00:00
Evgeny Poberezkin e5011ad150 Merge branch 'stable' into stable-android 2024-01-31 09:47:35 +00:00
Evgeny Poberezkin 0d4d08e52b core: unify dependencies for GHC 8.10.7 and 9.6.3 (#3774) 2024-01-30 23:18:37 +00:00
Evgeny Poberezkin 7f8c7b2d99 fix merge 2024-01-24 22:46:56 +00:00
Evgeny Poberezkin f5b666ae6d Merge branch 'stable' into stable-android 2024-01-24 22:42:04 +00:00
Evgeny Poberezkin 1cfe51af45 Merge branch 'stable' into stable-android 2024-01-18 20:58:52 +00:00
Evgeny Poberezkin 68af58bda1 Merge branch 'stable' into stable-android 2024-01-10 12:17:06 +00:00
Evgeny Poberezkin f51796b81c Merge branch 'stable' into stable-android 2024-01-09 20:20:37 +00:00
Evgeny Poberezkin 995618cf1c Merge branch 'stable' into stable-android 2024-01-09 10:40:34 +00:00
Evgeny Poberezkin 68baa18af8 Merge branch 'stable' into stable-android 2024-01-06 11:48:49 +00:00
Evgeny Poberezkin a4f1c9c915 Merge branch 'stable' into stable-android 2023-12-30 10:21:07 +00:00
Evgeny Poberezkin b8141c3555 Merge branch 'master-android' into stable-android 2023-12-28 09:18:12 +00:00
Evgeny Poberezkin 47214e33ea Merge branch 'master-ghc8107' into master-android 2023-12-27 21:02:49 +00:00
Evgeny Poberezkin ff239d81e5 Merge branch 'master' into master-ghc8107 2023-12-27 21:02:11 +00:00
Evgeny Poberezkin 0a6e47dd61 Merge branch 'master' into master-ghc8107 2023-12-27 20:57:25 +00:00
Evgeny Poberezkin ee75219ed3 core: 5.4.2.1 (simplexmq 5.5.0.2) 2023-12-27 20:57:05 +00:00
Evgeny Poberezkin 2e337a753f Merge branch 'master-ghc8107' into master-android 2023-12-27 14:09:42 +00:00
Evgeny Poberezkin 9918d91def Merge branch 'master' into master-ghc8107 2023-12-27 14:09:23 +00:00
Evgeny Poberezkin 82dd5751c1 Merge branch 'master-ghc8107' into master-android 2023-12-26 21:27:06 +00:00
Evgeny Poberezkin de637fab50 Merge branch 'master' into master-ghc8107 2023-12-26 21:12:36 +00:00
Evgeny Poberezkin cb49f6f01d Merge branch 'master-ghc8107' into master-android 2023-12-23 19:57:27 +00:00
Evgeny Poberezkin 6d0a83aa58 Merge branch 'master' into master-ghc8107 2023-12-23 19:57:08 +00:00
Evgeny Poberezkin 05f57e98ad Merge branch 'master-ghc8107' into master-android 2023-12-23 17:25:14 +00:00
Evgeny Poberezkin 6a66525927 Merge branch 'master' into master-ghc8107 2023-12-23 17:24:47 +00:00
Evgeny Poberezkin 9530a6055a Merge branch 'master-ghc8107' into master-android 2023-12-23 15:12:49 +00:00
Evgeny Poberezkin f5ed8debcc Merge branch 'master' into master-ghc8107 2023-12-23 15:11:38 +00:00
Evgeny Poberezkin 355d2449c5 fix for GHC 8.10.7 2023-12-23 15:05:36 +00:00
Evgeny Poberezkin f7382cdd6f Merge branch 'master' into master-ghc8107 2023-12-23 13:52:44 +00:00
Evgeny Poberezkin dc8d10d068 core: limit process package version to 1.6.17 2023-12-21 10:54:54 +00:00
Evgeny Poberezkin 951245d33f Merge branch 'master-ghc8107' into master-android 2023-12-21 10:00:28 +00:00
Evgeny Poberezkin 521b901cc9 Merge branch 'master' into master-ghc8107 (without changes for iPhone7) 2023-12-21 09:59:25 +00:00
Evgeny Poberezkin 6311ba451b Merge branch 'master-ghc8107' into master-android 2023-12-21 00:45:32 +00:00
Evgeny Poberezkin 754c76d6fd Merge branch 'master' into master-ghc8107 2023-12-21 00:45:10 +00:00
Evgeny Poberezkin 545f110fb8 nix: fix direct-sqlcipher content hash 2023-12-13 20:29:36 +00:00
Evgeny Poberezkin 4c6387c854 nix: fix script 2023-12-13 20:28:32 +00:00
Evgeny Poberezkin aea7ff1c89 nix: fix script 2023-12-13 20:27:58 +00:00
Evgeny Poberezkin 77ac972e09 Merge branch 'master-ghc8107' into master-android 2023-12-13 12:36:06 +00:00
Evgeny Poberezkin 256f85024f Merge branch 'master' into master-ghc8107 2023-12-13 12:35:37 +00:00
Evgeny Poberezkin 870f9e42dd Merge branch 'master' into master-ghc8107 2023-12-12 20:09:39 +00:00
Evgeny Poberezkin b3544cfbc3 Merge branch 'stable' into stable-android 2023-12-12 10:12:42 +00:00
Evgeny Poberezkin 0a4a3a24e1 Merge branch 'master-ghc8107' into master-android 2023-12-11 17:36:51 +00:00
Evgeny Poberezkin 8a66390a78 fix for GHC 8.10.7 2023-12-11 16:14:56 +00:00
Evgeny Poberezkin 3d48eded3d Merge branch 'master-ghc8107' into master-android 2023-12-11 14:56:47 +00:00
Evgeny Poberezkin 6546426ec0 Merge branch 'master' into master-ghc8107 2023-12-11 14:56:25 +00:00
spaced4ndy 4a7ceb00fb Merge branch 'master-ghc8107' into master-android 2023-12-11 17:53:08 +04:00
spaced4ndy 53560378bb Merge branch 'master' into master-ghc8107 2023-12-11 17:52:48 +04:00
Evgeny Poberezkin bfa8717ed4 Merge branch 'master-android' into stable-android 2023-12-09 20:43:21 +00:00
Evgeny Poberezkin cdb3b6aafd Merge branch 'master-ghc8107' into master-android 2023-12-07 15:12:49 +00:00
Evgeny Poberezkin 9f3d3e8ba4 Merge branch 'master' into master-ghc8107 2023-12-07 15:00:23 +00:00
Evgeny Poberezkin 047aad592e Merge branch 'master-ghc8107' into master-android 2023-12-04 12:32:05 +00:00
Evgeny Poberezkin 087acd9180 changes to support GHC 8.10.7 (#3512)
* 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

* compatibility with GHC 8.10.7

* simplexmq

* fix test

* simplexmq, deps

* update sqlcipher deps in sha256nix

* fix index-state in cabal.project

* index-state

* remove import

* add cabal.project.freeze

* simplexmq

* remove freeze

* simplexmq

* bytestring,simplexmq

* template-haskell, simplexmq

* simplexmq

* simplexmq

* simplexmq

* mtl

* simplexmq

* remove duplicate index-state
2023-12-04 12:29:49 +00:00
Evgeny Poberezkin 0b822e4a5c Merge branch 'master' into master-ghc8107 2023-12-04 10:07:16 +00:00
Evgeny Poberezkin f8a469488e Merge branch 'master' into master-ghc8107 2023-12-02 12:30:36 +00:00
Evgeny Poberezkin 3b5e806418 Merge branch 'master' into master-ghc8107 2023-12-01 17:46:12 +00:00
spaced4ndy 79e208193a Merge branch 'master-ghc8107' into master-android 2023-11-30 21:11:25 +04:00
spaced4ndy ef5c13b1c1 Merge branch 'master' into master-ghc8107 2023-11-30 21:10:51 +04:00
spaced4ndy 38533213d2 Merge branch 'master' into master-ghc8107 2023-11-30 20:56:51 +04:00
spaced4ndy 5f1aa6fa9d Merge branch 'master-ghc8107' into master-android 2023-11-27 19:18:22 +04:00
spaced4ndy b0002fe07d Merge branch 'master' into master-ghc8107 2023-11-27 19:17:39 +04:00
Evgeny Poberezkin ef21fd1d26 Merge branch 'master-ghc8107' into master-android 2023-11-26 19:59:46 +00:00
Evgeny Poberezkin 4a311b9578 fix for ghc 8.10.7 2023-11-26 19:34:39 +00:00
Evgeny Poberezkin b8da5e225b Merge branch 'master' into master-ghc8107 2023-11-26 18:53:40 +00:00
Evgeny Poberezkin 8b3300e197 Merge branch 'master-android' into stable-android 2023-11-26 13:14:27 +00:00
Evgeny Poberezkin f27de052cf Merge branch 'master' into master-android 2023-11-26 11:51:16 +00:00
Evgeny Poberezkin 5cc537f14c Merge branch 'master' into master-ghc8107 2023-11-26 11:50:50 +00:00
Evgeny Poberezkin dc8ca4cf89 Merge tag 'v5.4.0' into master-android 2023-11-25 11:23:23 +00:00
Evgeny Poberezkin b62dd801f1 Merge branch 'master-ghc8107' into master-android 2023-11-24 20:02:05 +00:00
Evgeny Poberezkin 0c096e2c89 Merge branch 'master' into master-ghc8107 2023-11-24 19:00:30 +00:00
Evgeny Poberezkin cc127e56fe Merge branch 'master' into master-android 2023-11-23 16:23:02 +00:00
Evgeny Poberezkin 1781495ee3 Merge branch 'master' into master-ghc8107 2023-11-23 16:22:46 +00:00
Evgeny Poberezkin 831231d8e6 Merge branch 'master-ghc8107' into master-android 2023-11-21 21:16:04 +00:00
Evgeny Poberezkin 45102442f4 Merge branch 'master' into master-ghc8107 2023-11-21 21:15:10 +00:00
spaced4ndy f323c8e112 Merge branch 'master-ghc8107' into master-android 2023-11-21 19:42:28 +04:00
spaced4ndy 3bdc6b5e28 Merge branch 'master' into master-ghc8107 2023-11-21 19:41:06 +04:00
Evgeny Poberezkin d8373262bc Merge branch 'master-ghc8107' into master-android 2023-11-21 00:01:20 +00:00
Evgeny Poberezkin 3597d34716 Merge branch 'master' into master-ghc8107 2023-11-21 00:00:59 +00:00
Evgeny Poberezkin bd4259e89e update hpack 2023-11-20 14:43:20 +00:00
Evgeny Poberezkin 55ead740cc update hpack 2023-11-20 14:43:05 +00:00
Evgeny Poberezkin 5ef0eda2d7 Merge branch 'master-ghc8107' into master-android 2023-11-20 14:30:27 +00:00
Evgeny Poberezkin 49a9b0e7d6 update hpack version 2023-11-20 14:30:10 +00:00
Evgeny Poberezkin 45ada450a2 Merge branch 'master-ghc8107' into master-android 2023-11-20 13:24:07 +00:00
Evgeny Poberezkin 307a1b3c5e fix for ghc 8.10.7 2023-11-20 13:23:45 +00:00
Evgeny Poberezkin ed6b3bbead Merge branch 'master' into master-ghc8107 2023-11-20 13:01:22 +00:00
spaced4ndy 901610eec5 Merge branch 'master-ghc8107' into master-android 2023-11-20 14:08:10 +04:00
spaced4ndy 7d4127c51d Merge branch 'master' into master-ghc8107 2023-11-20 14:07:08 +04:00
Evgeny Poberezkin 13215d91d7 Merge branch 'master-ghc8107' into master-android 2023-11-20 00:07:12 +00:00
Evgeny Poberezkin e1a8099474 fix for GHC 8.10.7 2023-11-20 00:06:45 +00:00
Evgeny Poberezkin daa8d9bb21 Merge branch 'master' into master-ghc8107 2023-11-19 23:42:13 +00:00
Evgeny Poberezkin 5fcbade1bc Merge branch 'master-ghc8107' into master-android 2023-11-17 11:49:35 +00:00
Evgeny Poberezkin 3937ffa9a6 Merge branch 'master' into master-ghc8107 2023-11-17 11:47:52 +00:00
Evgeny Poberezkin 80ddb50e1c Merge branch 'master-ghc8107' into master-android 2023-11-16 18:55:17 +00:00
Evgeny Poberezkin f6e66f1c53 Merge branch 'master' into master-ghc8107 2023-11-16 18:13:02 +00:00
Evgeny Poberezkin 0c23ff9ae3 Merge branch 'master-ghc8107' into master-android 2023-11-11 13:57:32 +00:00
Evgeny Poberezkin 1570bc2b99 Merge branch 'master' into master-ghc8107 2023-11-11 13:56:53 +00:00
Evgeny Poberezkin 1e2104cabf Merge branch 'master-ghc8107' into master-android 2023-11-11 09:52:07 +00:00
Evgeny Poberezkin f3014f258d Merge branch 'master' into master-ghc8107 2023-11-11 09:51:42 +00:00
Evgeny Poberezkin f0991cc0ba Merge branch 'master-ghc8107' into master-android 2023-11-10 21:22:19 +00:00
Evgeny Poberezkin 74b78a8d7b Merge branch 'master' into master-ghc8107 2023-11-10 21:11:08 +00:00
Evgeny Poberezkin 82cd70a75c Merge branch 'master-ghc8107' into master-android 2023-11-10 21:08:11 +00:00
Evgeny Poberezkin fe4eb7b5af Merge branch 'master' into master-ghc8107 without changes, to skip update for ghc 9.6.3 2023-11-10 21:04:20 +00:00
Evgeny Poberezkin c459e71d02 Merge branch 'master-ghc8107' into master-android 2023-11-06 11:26:40 +00:00
Evgeny Poberezkin 2516d5a393 Merge branch 'master' into master-ghc8107 2023-11-06 11:26:22 +00:00
spaced4ndy 477d98d75a Merge branch 'master-ghc8107' into master-android 2023-11-06 11:42:39 +04:00
spaced4ndy 4253cd7fb9 Merge branch 'master' into master-ghc8107 2023-11-06 11:41:55 +04:00
Evgeny Poberezkin ca78958667 Merge branch 'master-ghc8107' into master-android 2023-11-04 13:39:35 +00:00
Evgeny Poberezkin 1f5b80d560 fix for ghc8107 2023-11-04 13:37:25 +00:00
Evgeny Poberezkin 2de111e76c Merge branch 'master' into master-ghc8107 2023-11-04 13:02:08 +00:00
spaced4ndy 8343285d93 Merge branch 'master-ghc8107' into master-android 2023-10-30 21:01:16 +04:00
spaced4ndy 5dbe2b2745 Merge branch 'master' into master-ghc8107 2023-10-30 21:00:11 +04:00
Evgeny Poberezkin fb9485190d Merge branch 'master-ghc8107' into master-android 2023-10-29 18:27:00 +00:00
Evgeny Poberezkin 6881600e06 Merge branch 'master' into master-ghc8107 2023-10-29 18:24:13 +00:00
spaced4ndy 9ed723bafa Merge branch 'master-ghc8107' into master-android 2023-10-25 10:48:37 +04:00
spaced4ndy 9ded1c9821 Merge branch 'master' into master-ghc8107 2023-10-25 10:47:35 +04:00
spaced4ndy bb374c68b1 Merge branch 'master-ghc8107' into master-android 2023-10-24 18:14:22 +04:00
spaced4ndy c3e82a6a4e Merge branch 'master' into master-ghc8107 2023-10-24 18:13:56 +04:00
spaced4ndy 7c12e82042 Merge branch 'master-ghc8107' into master-android 2023-10-24 17:41:19 +04:00
spaced4ndy e7e66ff873 Merge branch 'master' into master-ghc8107 2023-10-24 17:40:55 +04:00
Evgeny Poberezkin c4d7e5307c Merge branch 'master-ghc8107' into master-android 2023-10-22 15:35:55 +01:00
Evgeny Poberezkin d6b9a45a39 Merge branch 'master' into master-ghc8107 2023-10-22 15:10:33 +01:00
Evgeny Poberezkin 7fd3b4d6ba Merge branch 'master-ghc8107' into master-android 2023-10-18 22:45:11 +01:00
Evgeny Poberezkin 4004aafbc5 Merge branch 'master' into master-ghc8107 2023-10-18 22:44:27 +01:00
spaced4ndy 95008eeeaf Merge branch 'master-ghc8107' into master-android 2023-10-16 20:05:49 +04:00
spaced4ndy c7a8992043 core: fix compilation for ghc 8.10.7 2023-10-16 20:05:13 +04:00
spaced4ndy ea2b5f2ccf Merge branch 'master-ghc8107' into master-android 2023-10-16 19:28:48 +04:00
spaced4ndy ed9f277421 Merge branch 'master' into master-ghc8107 2023-10-16 19:28:06 +04:00
Evgeny Poberezkin 5c14c3b349 Merge branch 'master-ghc8107' into master-android 2023-10-15 18:54:16 +01:00
Evgeny Poberezkin d8fb31f167 Merge branch 'master' into master-ghc8107 2023-10-15 18:53:23 +01:00
Evgeny Poberezkin 02db38ffd3 Merge branch 'master-ghc8107' into master-android 2023-10-11 21:58:16 +01:00
Evgeny Poberezkin 7692195bfa core: fix for ghc 8.10.7 2023-10-11 21:57:53 +01:00
Evgeny Poberezkin c435cbdc7b Merge branch 'master-ghc8107' into master-android 2023-10-11 21:28:43 +01:00
Evgeny Poberezkin effc281271 Merge branch 'master' into master-ghc8107 2023-10-11 21:27:21 +01:00
spaced4ndy 41eb2e5689 Merge branch 'master-ghc8107' into master-android 2023-10-11 13:22:30 +04:00
spaced4ndy 67d74a0a27 Merge branch 'master' into master-ghc8107 2023-10-11 13:21:46 +04:00
Evgeny Poberezkin c60af078d7 Merge branch 'stable' into stable-android 2023-10-09 19:55:51 +01:00
Evgeny Poberezkin f66405e79b Merge branch 'master-ghc8107' into master-android 2023-10-09 17:31:56 +01:00
Evgeny Poberezkin 74d186af16 Merge branch 'master' into master-ghc8107 2023-10-09 17:31:27 +01:00
Evgeny Poberezkin e4fbe66d95 Merge branch 'stable' into stable-android 2023-10-09 14:36:03 +01:00
Evgeny Poberezkin 187fef0c5a Merge branch 'master-ghc8107' into master-android 2023-10-09 14:05:21 +01:00
Evgeny Poberezkin 4782cab507 Merge branch 'master' into master-ghc8107 2023-10-09 14:05:04 +01:00
Evgeny Poberezkin cb52d75ff0 Merge branch 'stable' into stable-android 2023-10-08 17:31:23 +01:00
Evgeny Poberezkin bcbee67709 Merge branch 'master-ghc8107' into master-android 2023-10-08 08:38:40 +01:00
Evgeny Poberezkin 2501cbe55d Merge branch 'master' into master-ghc8107 2023-10-08 08:38:02 +01:00
Evgeny Poberezkin 2bd049db87 Merge branch 'master-ghc8107' into master-android 2023-10-07 21:10:22 +01:00
Evgeny Poberezkin 6b8b9ab4fd Merge branch 'master' into master-ghc8107 2023-10-07 19:06:38 +01:00
Evgeny Poberezkin 30db24265e Merge branch 'master-ghc8107' into master-android 2023-10-02 23:04:44 +01:00
Evgeny Poberezkin 316d605899 Merge branch 'master' into master-ghc8107 2023-10-02 23:04:13 +01:00
Evgeny Poberezkin b4257f7767 Merge branch 'master-ghc8107' into master-android 2023-10-01 13:21:50 +01:00
Evgeny Poberezkin a3f2d5c919 Merge branch 'master' into master-ghc8107 2023-10-01 13:20:06 +01:00
Evgeny Poberezkin cf46469cd5 Merge branch 'master-ghc8107' into master-android 2023-10-01 11:19:50 +01:00
Evgeny Poberezkin 0312fde818 Merge branch 'master' into master-ghc8107 2023-10-01 11:19:27 +01:00
Evgeny Poberezkin 9defa44f0c Merge branch 'master-ghc8107' into master-android 2023-09-29 13:15:23 +01:00
Evgeny Poberezkin 915b53054c Merge branch 'master' into master-ghc8107 2023-09-29 13:14:57 +01:00
Evgeny Poberezkin f81557b4fd Merge branch 'master-ghc8107' into master-android 2023-09-27 22:10:06 +01:00
Evgeny Poberezkin e273bd1239 Merge branch 'master' into master-ghc8107 2023-09-27 22:04:00 +01:00
spaced4ndy a63caf4640 Merge branch 'master-ghc8107' into master-android 2023-09-27 20:38:36 +04:00
spaced4ndy e7f0234134 Merge branch 'master' into master-ghc8107 2023-09-27 20:11:39 +04:00
Evgeny Poberezkin 340552321e Merge branch 'master-ghc8107' into master-android 2023-09-27 16:07:24 +01:00
Evgeny Poberezkin 98a3fc214d Merge branch 'master' into master-ghc8107 2023-09-27 16:04:25 +01:00
Evgeny Poberezkin 6a578cfe3c Merge branch 'master-ghc8107' into master-android 2023-09-25 16:53:04 +01:00
Evgeny Poberezkin dacc075fe8 Merge branch 'master' into master-ghc8107 2023-09-25 16:52:33 +01:00
spaced4ndy 55418e2bc0 Merge branch 'master-ghc8107' into master-android 2023-09-25 17:43:59 +04:00
spaced4ndy f2b5c0f3a8 Merge branch 'master' into master-ghc8107 2023-09-25 17:43:37 +04:00
spaced4ndy 5ebdf5dba9 Merge branch 'master-ghc8107' into master-android 2023-09-25 17:07:14 +04:00
spaced4ndy 8e045764df Merge branch 'master' into master-ghc8107 2023-09-25 16:40:08 +04:00
Evgeny Poberezkin 503d3d77e6 Merge branch 'master' into master-ghc8107 2023-09-23 08:47:28 +01:00
Evgeny Poberezkin 81bd7d97c5 Merge branch 'master' into master-ghc8107 2023-09-22 17:21:54 +01:00
Evgeny Poberezkin 8f57925067 compatibility with GHC 8.10.7 2023-09-22 14:01:25 +01:00
Evgeny Poberezkin 9bf99db82e Merge branch 'master' into master-ghc8107 2023-09-22 13:46:50 +01:00
Evgeny Poberezkin 5615cdbf1a Merge branch 'master' into master-android 2023-09-21 17:04:47 +01:00
Evgeny Poberezkin d802ae0058 Merge branch 'master' into master-android 2023-09-21 12:06:10 +01:00
Evgeny Poberezkin 8f2278198c Merge branch 'master' into master-android 2023-09-20 14:55:25 +01:00
spaced4ndy 10937a5a4e Merge branch 'master' into master-android 2023-09-20 17:36:53 +04:00
Evgeny Poberezkin 6aff6e9804 Merge branch 'master-ghc9' into master-ghc8107 2023-09-18 21:56:35 +01:00
Evgeny Poberezkin 95477cae7e core: use commit from simplexmq branch master-ghc8107 2023-09-18 21:45:50 +01:00
620 changed files with 66027 additions and 25081 deletions
+16 -14
View File
@@ -102,7 +102,7 @@ jobs:
uses: actions/checkout@v3
- name: Setup Haskell
uses: haskell-actions/setup@v2
uses: haskell/actions/setup@v2
with:
ghc-version: ${{ matrix.ghc }}
cabal-version: "3.10.1.0"
@@ -124,12 +124,12 @@ jobs:
run: |
echo "ignore-project: False" >> cabal.project.local
echo "package simplexmq" >> cabal.project.local
echo " extra-include-dirs: /opt/homebrew/opt/openssl@1.1/include" >> cabal.project.local
echo " extra-lib-dirs: /opt/homebrew/opt/openssl@1.1/lib" >> cabal.project.local
echo " extra-include-dirs: /opt/homebrew/opt/openssl@3.0/include" >> cabal.project.local
echo " extra-lib-dirs: /opt/homebrew/opt/openssl@3.0/lib" >> cabal.project.local
echo "" >> cabal.project.local
echo "package direct-sqlcipher" >> cabal.project.local
echo " extra-include-dirs: /opt/homebrew/opt/openssl@1.1/include" >> cabal.project.local
echo " extra-lib-dirs: /opt/homebrew/opt/openssl@1.1/lib" >> cabal.project.local
echo " extra-include-dirs: /opt/homebrew/opt/openssl@3.0/include" >> cabal.project.local
echo " extra-lib-dirs: /opt/homebrew/opt/openssl@3.0/lib" >> cabal.project.local
echo " flags: +openssl" >> cabal.project.local
- name: Unix prepare cabal.project.local for Mac
@@ -138,21 +138,21 @@ jobs:
run: |
echo "ignore-project: False" >> cabal.project.local
echo "package simplexmq" >> cabal.project.local
echo " extra-include-dirs: /usr/local/opt/openssl@1.1/include" >> cabal.project.local
echo " extra-lib-dirs: /usr/local/opt/openssl@1.1/lib" >> cabal.project.local
echo " extra-include-dirs: /usr/local/opt/openssl@3.0/include" >> cabal.project.local
echo " extra-lib-dirs: /usr/local/opt/openssl@3.0/lib" >> cabal.project.local
echo "" >> cabal.project.local
echo "package direct-sqlcipher" >> cabal.project.local
echo " extra-include-dirs: /usr/local/opt/openssl@1.1/include" >> cabal.project.local
echo " extra-lib-dirs: /usr/local/opt/openssl@1.1/lib" >> cabal.project.local
echo " extra-include-dirs: /usr/local/opt/openssl@3.0/include" >> cabal.project.local
echo " extra-lib-dirs: /usr/local/opt/openssl@3.0/lib" >> cabal.project.local
echo " flags: +openssl" >> cabal.project.local
- name: Install AppImage dependencies
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
- name: Install openssl for Mac
if: matrix.os == 'macos-latest' || matrix.os == 'macos-13'
run: brew install pkg-config
run: brew install openssl@3.0
- name: Unix prepare cabal.project.local for Ubuntu
if: matrix.os == 'ubuntu-20.04' || matrix.os == 'ubuntu-22.04'
@@ -230,7 +230,7 @@ jobs:
APPLE_SIMPLEX_NOTARIZATION_APPLE_ID: ${{ secrets.APPLE_SIMPLEX_NOTARIZATION_APPLE_ID }}
APPLE_SIMPLEX_NOTARIZATION_PASSWORD: ${{ secrets.APPLE_SIMPLEX_NOTARIZATION_PASSWORD }}
run: |
scripts/ci/build-desktop-mac.sh
scripts/build-desktop-mac.sh
path=$(echo $PWD/apps/multiplatform/release/main/dmg/SimpleX-*.dmg)
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
@@ -310,7 +310,9 @@ jobs:
# Unix /
# / Windows
# rm -rf dist-newstyle/src/direct-sq* is here because of the bug in cabal's dependency which prevents second build from finishing
# * In powershell multiline commands do not fail if individual commands fail - https://github.community/t/multiline-commands-on-windows-do-not-fail-if-individual-commands-fail/16753
# * And GitHub Actions does not support parameterizing shell in a matrix job - https://github.community/t/using-matrix-to-specify-shell-is-it-possible/17065
- name: 'Setup MSYS2'
if: matrix.os == 'windows-latest'
@@ -334,7 +336,7 @@ jobs:
run: |
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')
openssl_windows_style_path=$(echo `pwd`/dist-newstyle/openssl-3.0.15 | sed 's#/\([a-zA-Z]\)#\1:#' | sed 's#/#\\#g')
rm cabal.project.local 2>/dev/null || true
echo "ignore-project: False" >> cabal.project.local
echo "package direct-sqlcipher" >> cabal.project.local
+1
View File
@@ -10,6 +10,7 @@ on:
- blog/**
- docs/**
- .github/workflows/web.yml
- PRIVACY.md
jobs:
build:
+1
View File
@@ -61,6 +61,7 @@ website/package/generated*
# Ignore build tool output, e.g. code coverage
website/.nyc_output/
website/coverage/
result
# Ignore API documentation
website/api-docs/
+1 -1
View File
@@ -29,7 +29,7 @@ RUN cp ./scripts/cabal.project.local.linux ./cabal.project.local
# Compile simplex-chat
RUN cabal update
RUN cabal build exe:simplex-chat
RUN cabal build exe:simplex-chat --constraint 'simplexmq +client_library'
# Strip the binary from debug symbols to reduce size
RUN bin=$(find /project/dist-newstyle -name "simplex-chat" -type f -executable) && \
+109 -59
View File
@@ -3,63 +3,93 @@ layout: layouts/privacy.html
permalink: /privacy/index.html
---
# SimpleX Chat Privacy Policy and Conditions of Use
# SimpleX Chat Operators Privacy Policy and Conditions of Use
SimpleX Chat is the first communication network based on a new protocol stack that builds on the same ideas of complete openness and decentralization as email and web, with the focus on providing security and privacy of communications, and without compromising on usability.
## Summary
SimpleX Chat communication protocol is the first protocol that has no user profile IDs of any kind, not even random numbers, cryptographic keys or hashes that identify the users. SimpleX Chat apps allow their users to send messages and files via relay server infrastructure. Relay server owners and providers do not have any access to your messages, thanks to double-ratchet end-to-end encryption algorithm (also known as Signal algorithm - do not confuse with Signal protocols or platform) and additional encryption layers, and they also have no access to your profile and contacts - as they do not provide any user accounts.
[Introduction](#introduction) and [General principles](#general-principles) cover SimpleX Chat network design, the network operators, and the principles of privacy and security provided by SimpleX network.
[Privacy policy](#privacy-policy) covers:
- data stored only on your device - [your profiles](#user-profiles), delivered [messages and files](#messages-and-files). You can transfer this information to another device, and you are responsible for its preservation - if you delete the app it will be lost.
- [private message delivery](#private-message-delivery) that protects your IP address and connection graph from the destination servers.
- [undelivered messages and files](#storage-of-messages-and-files-on-the-servers) stored on the servers.
- [how users connect](#connections-with-other-users) without any user profile identifiers.
- [iOS push notifications](#ios-push-notifications) privacy limitations.
- [user support](#user-support), [SimpleX directory](#simplex-directory) and [any other data](#another-information-stored-on-the-servers) that may be stored on the servers.
- [preset server operators](#preset-server-operators) and the [information they may share](#information-preset-server-operators-may-share).
- [source code license](#source-code-license) and [updates to this document](#updates).
[Conditions of Use](#conditions-of-use-of-software-and-infrastructure) are the conditions you need to accept to use SimpleX Chat applications and the relay servers of preset operators. Their purpose is to protect the users and preset server operators.
*Please note*: this summary and any links in this document are provided for information only - they are not a part of the Privacy Policy and Conditions of Use.
## Introduction
SimpleX Chat (also referred to as SimpleX) is the first communication network based on a new protocol stack that builds on the same ideas of complete openness and decentralization as email and web, with the focus on providing security and privacy of communications, and without compromising on usability.
SimpleX messaging protocol is the first protocol that has no user profile IDs of any kind, not even random numbers, cryptographic keys or hashes that identify the users. SimpleX apps allow their users to send messages and files via relay server infrastructure. Relay server owners and operators do not have any access to your messages, thanks to double-ratchet end-to-end encryption algorithm (also known as Signal algorithm - do not confuse with Signal protocols or platform) and additional encryption layers, and they also have no access to your profile and contacts - as they do not host user accounts.
Double ratchet algorithm has such important properties as [forward secrecy](/docs/GLOSSARY.md#forward-secrecy), sender [repudiation](/docs/GLOSSARY.md#) and break-in recovery (also known as [post-compromise security](/docs/GLOSSARY.md#post-compromise-security)).
If you believe that any part of this document is not aligned with our mission or values, please raise it with us via [email](mailto:chat@simplex.chat) or [chat](https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23%2F%3Fv%3D1%26dh%3DMCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion).
If you believe that any part of this document is not aligned with SimpleX network mission or values, please raise it via [email](mailto:chat@simplex.chat) or [chat](https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23%2F%3Fv%3D1%26dh%3DMCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion).
## Privacy Policy
SimpleX Chat Ltd uses the best industry practices for security and encryption to provide client and server software for secure [end-to-end encrypted](/docs/GLOSSARY.md#end-to-end-encryption) messaging via private connections. This encryption cannot be compromised by the relays servers, even if they are modified or compromised, via [man-in-the-middle attack](/docs/GLOSSARY.md#man-in-the-middle-attack), unlike most other communication platforms, services and networks.
### General principles
SimpleX Chat software is built on top of SimpleX messaging and application protocols, based on a new message routing protocol allowing to establish private connections without having any kind of addresses or other identifiers assigned to its users - it does not use emails, phone numbers, usernames, identity keys or any other user profile identifiers to pass messages between the user applications.
SimpleX network software uses the best industry practices for security and encryption to provide client and server software for secure [end-to-end encrypted](/docs/GLOSSARY.md#end-to-end-encryption) messaging via private connections. This encryption is protected from being compromised by the relays servers, even if they are modified or compromised, via [man-in-the-middle attack](/docs/GLOSSARY.md#man-in-the-middle-attack).
SimpleX Chat software is similar in its design approach to email clients and browsers - it allows you to have full control of your data and freely choose the relay server providers, in the same way you choose which website or email provider to use, or use your own relay servers, simply by changing the configuration of the client software. The only current restriction to that is Apple push notifications - at the moment they can only be delivered via the preset servers that we operate, as explained below. We are exploring the solutions to deliver push notifications to iOS devices via other providers or users' own servers.
SimpleX software is built on top of SimpleX messaging and application protocols, based on a new message routing protocol allowing to establish private connections without having identifiers assigned to its users - it does not use emails, phone numbers, usernames, identity keys or any other user profile identifiers to pass messages between the user applications.
While SimpleX Chat Ltd is not a communication service provider, and provide public preset relays "as is", as experimental, without any guarantees of availability or data retention, we are committed to maintain a high level of availability, reliability and security of these preset relays. We will be adding alternative preset infrastructure providers to the software in the future, and you will continue to be able to use any other providers or your own servers.
SimpleX software is similar in its design approach to email clients and browsers - it allows you to have full control of your data and freely choose the relay server operators, in the same way you choose which website or email provider to use, or use your own relay servers, simply by changing the configuration of the client software. The only current restriction to that is Apple push notifications - at the moment they can only be delivered via the servers operated by SimpleX Chat Ltd, as explained below. We are exploring the solutions to deliver push notifications to iOS devices via other providers or users' own servers.
We see users and data sovereignty, and device and provider portability as critically important properties for any communication system.
SimpleX network operators are not communication service provider, and provide public relays "as is", as experimental, without any guarantees of availability or data retention. The operators of the relay servers preset in the app ("Preset Server Operators"), including SimpleX Chat Ltd, are committed to maintain a high level of availability, reliability and security. SimpleX client apps can have multiple preset relay server operators that you can opt-in or opt-out of using. You are and will continue to be able to use any other operators or your own servers.
SimpleX Chat security assessment was done in October 2022 by [Trail of Bits](https://www.trailofbits.com/about), and most fixes were released in v4.2 see [the announcement](/blog/20221108-simplex-chat-v4.2-security-audit-new-website.md).
SimpleX network design is based on the principles of users and data sovereignty, and device and operator portability.
The implementation security assessment of SimpleX cryptography and networking was done in October 2022 by [Trail of Bits](https://www.trailofbits.com/about), and most fixes were released in v4.2 see [the announcement](/blog/20221108-simplex-chat-v4.2-security-audit-new-website.md).
The cryptographic review of SimpleX protocols design was done in July 2024 by Trail of Bits see [the announcement](/blog/20241014-simplex-network-v6-1-security-review-better-calls-user-experience.md).
### Your information
#### User profiles
Servers used by SimpleX Chat apps do not create, store or identify user profiles. The profiles you can create in the app are local to your device, and can be removed at any time via the app.
Servers used by SimpleX Chat apps do not create, store or identify user chat profiles. The profiles you can create in the app are local to your device, and can be removed at any time via the app.
When you create the local profile, no records are created on any of the relay servers, and infrastructure providers, whether SimpleX Chat Ltd or any other, have no access to any part of your information, and even to the fact that you created a profile - it is a local record stored only on your device. That means that if you delete the app, and have no backup, you will permanently lose all your data and the private connections you created with other software users.
When you create the local profile, no records are created on any of the relay servers, and infrastructure operators, whether preset in the app or any other, have no access to any part of your information, and even to the fact that you created a profile - it is a local record stored only on your device. That means that if you delete the app, and have no backup, you will permanently lose all your data and the private connections you created with other software users.
You can transfer the profile to another device by creating a backup of the app data and restoring it on the new device, but you cannot use more than one device with the copy of the same profile at the same time - it will disrupt any active conversations on either or both devices, as a security property of end-to-end encryption.
#### Messages and Files
SimpleX relay servers cannot decrypt or otherwise access the content or even the size of your messages and files you send or receive. Each message is padded to a fixed size of 16kb. Each file is sent in chunks of 64kb, 256kb, 1mb or 8mb via all or some of the configured file relay servers. Both messages and files are sent end-to-end encrypted, and the servers do not have technical means to compromise this encryption, because part of the [key exchange](/docs/GLOSSARY.md#key-exchange) happens out-of-band.
SimpleX relay servers cannot decrypt or otherwise access the content or even the size of your messages and files you send or receive. Each message is padded to a fixed size of 16kb. Each file is sent in chunks of 64kb, 256kb, 1mb or 4mb via all or some of the configured file relay servers. Both messages and files are sent end-to-end encrypted, and the servers do not have technical means to compromise this encryption, because part of the [key exchange](/docs/GLOSSARY.md#key-exchange) happens out-of-band.
Your message history is stored only on your own device and the devices of your contacts. While the recipients' devices are offline, messaging relay servers temporarily store end-to-end encrypted messages you can configure which relay servers are used to receive the messages from the new contacts, and you can manually change them for the existing contacts too.
You do not have control over which servers are used to send messages to your contacts - they are chosen by them. To send messages your client needs to connect to these servers, therefore the servers chosen by your contacts can observe your IP address. You can use VPN or some overlay network (e.g., Tor) to hide your IP address from the servers chosen by your contacts. In the near future we will add the layer in the messaging protocol that will route sent message via the relays chosen by you as well.
#### Private message delivery
The messages are permanently removed from the used relay servers as soon as they are delivered, as long as these servers used unmodified published code. Undelivered messages are deleted after the time that is configured in the messaging servers you use (21 days for preset messaging servers).
You do not have control over which servers are used to send messages to your contacts - these servers are chosen by your contacts. To send messages your client by default uses configured servers to forward messages to the destination servers, thus protecting your IP address from the servers chosen by your contacts.
In case you use preset servers of more than one operator, the app will prefer to use a server of an operator different from the operator of the destination server to forward messages, preventing destination server to correlate messages as belonging to one client.
You can additionally use VPN or some overlay network (e.g., Tor) to hide your IP address from the servers chosen by you.
*Please note*: the clients allow changing configuration to connect to the destination servers directly. It is not recommended - if you make such change, your IP address will be visible to the destination servers.
#### Storage of messages and files on the servers
The messages are removed from the relay servers as soon as all messages of the file they were stored in are delivered and saving new messages switches to another file, as long as these servers use unmodified published code. Undelivered messages are also marked as delivered after the time that is configured in the messaging servers you use (21 days for preset messaging servers).
The files are stored on file relay servers for the time configured in the relay servers you use (48 hours for preset file servers).
If a messaging servers are restarted, the encrypted message can be stored in a backup file until it is overwritten by the next restart (usually within 1 week for preset relay servers).
As this software is fully open-source and provided under AGPLv3 license, all infrastructure providers and owners, and the developers of the client and server applications who use the SimpleX Chat source code, are required to publish any changes to this software under the same AGPLv3 license - including any modifications to the provided servers.
In addition to the AGPLv3 license terms, SimpleX Chat Ltd is committed to the software users that the preset relays that we provide via the apps will always be compiled from the [published open-source code](https://github.com/simplex-chat/simplexmq), without any modifications.
The encrypted messages can be stored for some time after they are delivered or expired (because servers use append-only logs for message storage). This time varies, and may be longer in connections with fewer messages, but it is usually limited to 1 month, including any backup storage.
#### Connections with other users
When you create a connection with another user, two messaging queues (you can think about them as mailboxes) are created on messaging relay servers (chosen by you and your contact each), that can be the preset servers or the servers that you and your contact configured in the app. SimpleX messaging protocol uses separate queues for direct and response messages, and the apps prefer to create these queues on two different relay servers for increased privacy, in case you have more than one relay server configured in the app, which is the default.
When you create a connection with another user, two messaging queues (you can think about them as mailboxes) are created on messaging relay servers (chosen by you and your contact each), that can be the preset servers or the servers that you and your contact configured in the app. SimpleX messaging protocol uses separate queues for direct and response messages, and the apps prefer to create these queues on two different relay servers, or, if available, the relays of two different operators, for increased privacy, in case you have more than one relay server configured in the app, which is the default.
SimpleX relay servers do not store information about which queues are linked to your profile on the device, and they do not collect any information that would allow infrastructure owners and providers to establish that these queues are related to your device or your profile - the access to each queue is authorized by two anonymous unique cryptographic keys, different for each queue, and separate for sender and recipient of the messages.
Preset and unmodified SimpleX relay servers do not store information about which queues are linked to your profile on the device, and they do not collect any information that would allow infrastructure owners and operators to establish that these queues are related to your device or your profile - the access to each queue is authorized by two anonymous unique cryptographic keys, different for each queue, and separate for sender and recipient of the messages.
#### Connection links privacy
@@ -75,6 +105,8 @@ You can always safely replace the initial part of the link `https://simplex.chat
#### iOS Push Notifications
This section applies only to the notification servers operated by SimpleX Chat Ltd.
When you choose to use instant push notifications in SimpleX iOS app, because the design of push notifications requires storing the device token on notification server, the notifications server can observe how many messaging queues your device has notifications enabled for, and approximately how many messages are sent to each queue.
Preset notification server cannot observe the actual addresses of these queues, as a separate address is used to subscribe to the notifications. It also cannot observe who sends messages to you. Apple push notifications servers can only observe how many notifications are sent to you, but not from how many contacts, or from which messaging relays, as notifications are delivered to your device end-to-end encrypted by one of the preset notification servers - these notifications only contain end-to-end encrypted metadata, not even encrypted message content, and they look completely random to Apple push notification servers.
@@ -83,93 +115,111 @@ You can read more about the design of iOS push notifications [here](./blog/20220
#### Another information stored on the servers
Additional technical information can be stored on our servers, including randomly generated authentication tokens, keys, push tokens, and other material that is necessary to transmit messages. SimpleX Chat design limits this additional technical information to the minimum required to operate the software and servers. To prevent server overloading or attacks, the servers can temporarily store data that can link to particular users or devices, including IP addresses, geographic location, or information related to the transport sessions. This information is not stored for the absolute majority of the app users, even for those who use the servers very actively.
Additional technical information can be stored on the network servers, including randomly generated authentication tokens, keys, push tokens, and other material that is necessary to transmit messages. SimpleX network design limits this additional technical information to the minimum required to operate the software and servers. To prevent server overloading or attacks, the servers can temporarily store data that can link to particular users or devices, including IP addresses, geographic location, or information related to the transport sessions. This information is not stored for the absolute majority of the app users, even for those who use the servers very actively.
#### SimpleX Directory
This section applies only to the experimental group directory operated by SimpleX Chat Ltd.
[SimpleX Directory](/docs/DIRECTORY.md) stores: your search requests, the messages and the members profiles in the registered groups. You can connect to SimpleX Directory via [this address](https://simplex.chat/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).
#### User Support
If you contact SimpleX Chat Ltd, any personal data you share with us is kept only for the purposes of researching the issue and contacting you about your case. We recommend contacting support [via chat](https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23%2F%3Fv%3D1%26dh%3DMCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion) when it is possible, and avoid sharing any personal information.
The app includes support contact operated by SimpleX Chat Ltd. If you contact support, any personal data you share is kept only for the purposes of researching the issue and contacting you about your case. We recommend contacting support [via chat](https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23%2F%3Fv%3D1%26dh%3DMCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion) when it is possible, and avoid sharing any personal information.
### Information we may share
### Preset Server Operators
SimpleX Chat Ltd operates preset relay servers using third parties. While we do not have access and cannot share any user data, these third parties may access the encrypted user messages (but NOT the actual unencrypted message content or size) as it is stored or transmitted via our servers. Hosting providers can also store IP addresses and other transport information as part of their logs.
Preset server operators will not share the information on their servers with each other, other than aggregate usage statistics.
We use a third party for email services - if you ask for support via email, your and SimpleX Chat Ltd email providers may access these emails according to their privacy policies and terms. When the request is sensitive, we recommend contacting us via SimpleX Chat or using encrypted email using PGP key published at [openpgp.org](https://keys.openpgp.org/search?q=chat%40simplex.chat).
Preset server operators will not provide general access to their servers or the data on their servers to each other.
The cases when SimpleX Chat Ltd may share the data temporarily stored on the servers:
Preset server operators will provide non-administrative access to control port of preset servers to SimpleX Chat Ltd, for the purposes of removing identified illegal content. This control port access only allows deleting known links and files, and access to aggregate statistics, but does NOT allow enumerating any information on the servers.
### Information Preset Server Operators May Share
The preset server operators use third parties. While they do not have access and cannot share any user data, these third parties may access the encrypted user messages (but NOT the actual unencrypted message content or size) as it is stored or transmitted via the servers. Hosting and network providers can also store IP addresses and other transport information as part of their logs.
SimpleX Chat Ltd uses a third party for email services - if you ask for support via email, your and SimpleX Chat Ltd email providers may access these emails according to their privacy policies and terms. When the request is sensitive, please contact us via SimpleX Chat apps or using encrypted email using PGP key published at [openpgp.org](https://keys.openpgp.org/search?q=chat%40simplex.chat).
The cases when the preset server operators may share the data temporarily stored on the servers:
- To meet any applicable law, or enforceable governmental request or court order.
- To enforce applicable terms, including investigation of potential violations.
- To detect, prevent, or otherwise address fraud, security, or technical issues.
- To protect against harm to the rights, property, or safety of software users, SimpleX Chat Ltd, or the public as required or permitted by law.
- To protect against harm to the rights, property, or safety of software users, operators of preset servers, or the public as required or permitted by law.
At the time of updating this document, we have never provided or have been requested the access to the preset relay servers or any information from the servers by any third parties. If we are ever requested to provide such access or information, we will follow the due legal process to limit any information shared with the third parties to the minimally required by law.
At the time of updating this document, the preset server operators have never provided or have been requested the access to the preset relay servers or any information from the servers by any third parties. If the preset server operators are ever requested to provide such access or information, they will follow the due legal process to limit any information shared with the third parties to the minimally required by law.
We will publish information we are legally allowed to share about such requests in the [Transparency reports](./docs/TRANSPARENCY.md).
Preset server operators will publish information they are legally allowed to share about such requests in the [Transparency reports](./docs/TRANSPARENCY.md).
### Source code license
As this software is fully open-source and provided under AGPLv3 license, all infrastructure owners and operators, and the developers of the client and server applications who use the SimpleX Chat source code, are required to publish any changes to this software under the same AGPLv3 license - including any modifications to the servers.
In addition to the AGPLv3 license terms, the preset relay server operators are committed to the software users that these servers will always be compiled from the [published open-source code](https://github.com/simplex-chat/simplexmq), without any modifications.
### Updates
We will update this Privacy Policy as needed so that it is current, accurate, and as clear as possible. Your continued use of our software applications and preset relays infrastructure confirms your acceptance of our updated Privacy Policy.
This Privacy Policy applies to SimpleX Chat Ltd and all other preset server operators you use in the app.
Please also read our Conditions of Use of Software and Infrastructure below.
This Privacy Policy may be updated as needed so that it is current, accurate, and as clear as possible. When it is updated, you will have to review and accept the changed policy within 30 days of such changes to continue using preset relay servers. Even if you fail to accept the changed policy, your continued use of SimpleX Chat software applications and preset relay servers confirms your acceptance of the updated Privacy Policy.
If you have questions about our Privacy Policy please contact us via [email](mailto:chat@simplex.chat) or [chat](https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23%2F%3Fv%3D1%26dh%3DMCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion).
Please also read The Conditions of Use of Software and Infrastructure below.
If you have questions about this Privacy Policy please contact SimpleX Chat Ltd via [email](mailto:chat@simplex.chat) or [chat](https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23%2F%3Fv%3D1%26dh%3DMCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion).
## Conditions of Use of Software and Infrastructure
You accept the Conditions of Use of Software and Infrastructure ("Conditions") by installing or using any of our software or using any of our server infrastructure (collectively referred to as "Applications"), whether preset in the software or not.
You accept the Conditions of Use of Software and Infrastructure ("Conditions") by installing or using any of SimpleX Chat software or using any of server infrastructure (collectively referred to as "Applications") operated by the Preset Server Operators, including SimpleX Chat Ltd, whether these servers are preset in the software or not.
**Minimal age**. You must be at least 13 years old to use our Applications. The minimum age to use our Applications without parental approval may be higher in your country.
**Minimal age**. You must be at least 13 years old to use SimpleX Chat Applications. The minimum age to use SimpleX Applications without parental approval may be higher in your country.
**Infrastructure**. Our Infrastructure includes preset messaging and file relay servers, and iOS push notification servers provided by SimpleX Chat Ltd for public use. Our infrastructure does not have any modifications from the [published open-source code](https://github.com/simplex-chat/simplexmq) available under AGPLv3 license. Any infrastructure provider, whether commercial or not, is required by the Affero clause (named after Affero Inc. company that pioneered the community-based Q&A sites in early 2000s) to publish any modifications under the same license. The statements in relation to Infrastructure and relay servers anywhere in this document assume no modifications to the published code, even in the cases when it is not explicitly stated.
**Infrastructure**. Infrastructure of the preset server operators includes messaging and file relay servers. SimpleX Chat Ltd also provides iOS push notification servers for public use. This infrastructure does not have any modifications from the [published open-source code](https://github.com/simplex-chat/simplexmq) available under AGPLv3 license. Any infrastructure provider, whether commercial or not, is required by the Affero clause (named after Affero Inc. company that pioneered the community-based Q&A sites in early 2000s) to publish any modifications under the same license. The statements in relation to Infrastructure and relay servers anywhere in this document assume no modifications to the published code, even in the cases when it is not explicitly stated.
**Client applications**. Our client application Software (referred to as "app" or "apps") also has no modifications compared with published open-source code, and any developers of the alternative client apps based on our code are required to publish any modifications under the same AGPLv3 license. Client applications should not include any tracking or analytics code, and do not share any information with SimpleX Chat Ltd or any other third parties. If you ever discover any tracking or analytics code, please report it to us, so we can remove it.
**Client applications**. SimpleX Chat client application Software (referred to as "app" or "apps") also has no modifications compared with published open-source code, and any developers of the alternative client apps based on SimpleX Chat code are required to publish any modifications under the same AGPLv3 license. Client applications should not include any tracking or analytics code, and do not share any tracking information with SimpleX Chat Ltd, preset server operators or any other third parties. If you ever discover any tracking or analytics code, please report it to SimpleX Chat Ltd, so it can be removed.
**Accessing the infrastructure**. For the efficiency of the network access, the client Software by default accesses all queues your app creates on any relay server within one user profile via the same network (TCP/IP) connection. At the cost of additional traffic this configuration can be changed to use different transport session for each connection. Relay servers do not collect information about which queues were created or accessed via the same connection, so the relay servers cannot establish which queues belong to the same user profile. Whoever might observe your network traffic would know which relay servers you use, and how much data you send, but not to whom it is sent - the data that leaves the servers is always different from the data they receive - there are no identifiers or ciphertext in common, even inside TLS encryption layer. Please refer to our [technical design document](https://github.com/simplex-chat/simplexmq/blob/master/protocol/overview-tjr.md) for more information about our privacy model and known security and privacy risks.
**Accessing the infrastructure**. For the efficiency of the network access, the client Software by default accesses all queues your app creates on any relay server within one user profile via the same network (TCP/IP) connection. At the cost of additional traffic this configuration can be changed to use different transport session for each connection. Relay servers do not collect information about which queues were created or accessed via the same connection, so the relay servers cannot establish which queues belong to the same user profile. Whoever might observe your network traffic would know which relay servers you use, and how much data you send, but not to whom it is sent - the data that leaves the servers is always different from the data they receive - there are no identifiers or ciphertext in common, even inside TLS encryption layer. Please refer to the [technical design document](https://github.com/simplex-chat/simplexmq/blob/master/protocol/overview-tjr.md) for more information about the privacy model and known security and privacy risks.
**Privacy of user data**. Servers do not retain any data we transmit for any longer than necessary to deliver the messages between apps. SimpleX Chat Ltd collects aggregate statistics across all its servers, as supported by published code and can be enabled by any infrastructure provider, but not any statistics per-user, or per geographic location, or per IP address, or per transport session. We do not have information about how many people use SimpleX Chat applications, we only know an approximate number of app installations and the aggregate traffic through the preset servers. In any case, we do not and will not sell or in any way monetize user data. Our future business model assumes charging for some optional Software features instead, in a transparent and fair way.
**Privacy of user data**. Servers do not retain any data you transmit for any longer than necessary to deliver the messages between apps. Preset server operators collect aggregate statistics across all their servers, as supported by published code and can be enabled by any infrastructure operator, but not any statistics per-user, or per geographic location, or per IP address, or per transport session. SimpleX Chat Ltd does not have information about how many people use SimpleX Chat applications, it only knows an approximate number of app installations and the aggregate traffic through the preset servers. In any case, preset server operators do not and will not sell or in any way monetize user data. The future business model assumes charging for some optional Software features instead, in a transparent and fair way.
**Operating our Infrastructure**. For the purpose of using our Software, if you continue using preset servers, you agree that your end-to-end encrypted messages are transferred via the preset servers in any countries where we have or use facilities and service providers or partners. The information about geographic location of the servers will be made available in the apps in the near future.
**Operating Infrastructure**. For the purpose of using SimpleX Chat Software, if you continue using preset servers, you agree that your end-to-end encrypted messages are transferred via the preset servers in any countries where preset server operators have or use facilities and service providers or partners. The information about geographic location and hosting providers of the preset messaging servers is available on server pages.
**Software**. You agree to downloading and installing updates to our Applications when they are available; they would only be automatic if you configure your devices in this way.
**Software**. You agree to downloading and installing updates to SimpleX Chat Applications when they are available; they would only be automatic if you configure your devices in this way.
**Traffic and device costs**. You are solely responsible for the traffic and device costs that you incur while using our Applications, and any associated taxes.
**Traffic and device costs**. You are solely responsible for the traffic and device costs that you incur while using SimpleX Chat Applications, and any associated taxes.
**Legal usage**. You agree to use our Applications only for legal purposes. You will not use (or assist others in using) our Applications in ways that: 1) violate or infringe the rights of Software users, SimpleX Chat Ltd, or others, including privacy, publicity, intellectual property, or other proprietary rights; 2) involve sending illegal communications, e.g. spam. While we cannot access content or identify messages or groups, in some cases the links to the illegal communications available via our Applications can be shared publicly on social media or websites. We reserve the right to remove such links from the preset servers and disrupt the conversations that send illegal content via our servers, whether they were reported by the users or discovered by our team.
**Legal usage**. You agree to use SimpleX Chat Applications only for legal purposes. You will not use (or assist others in using) the Applications in ways that: 1) violate or infringe the rights of Software users, SimpleX Chat Ltd, other preset server operators, or others, including privacy, publicity, intellectual property, or other proprietary rights; 2) involve sending illegal communications, e.g. spam. While server operators cannot access content or identify messages or groups, in some cases the links to the illegal communications can be shared publicly on social media or websites. Preset server operators reserve the right to remove such links from the preset servers and disrupt the conversations that send illegal content via their servers, whether they were reported by the users or discovered by the operators themselves.
**Damage to SimpleX Chat Ltd**. You must not (or assist others to) access, use, modify, distribute, transfer, or exploit our Applications in unauthorized manners, or in ways that harm Software users, SimpleX Chat Ltd, our Infrastructure, or any other systems. For example, you must not 1) access our Infrastructure or systems without authorization, in any way other than by using the Software; 2) disrupt the integrity or performance of our Infrastructure; 3) collect information about our users in any manner; or 4) sell, rent, or charge for our Infrastructure. This does not prohibit you from providing your own Infrastructure to others, whether free or for a fee, as long as you do not violate these Conditions and AGPLv3 license, including the requirement to publish any modifications of the relay server software.
**Damage to SimpleX Chat Ltd and Preset Server Operators**. You must not (or assist others to) access, use, modify, distribute, transfer, or exploit SimpleX Chat Applications in unauthorized manners, or in ways that harm Software users, SimpleX Chat Ltd, other preset server operators, their Infrastructure, or any other systems. For example, you must not 1) access preset operators' Infrastructure or systems without authorization, in any way other than by using the Software; 2) disrupt the integrity or performance of preset operators' Infrastructure; 3) collect information about the users in any manner; or 4) sell, rent, or charge for preset operators' Infrastructure. This does not prohibit you from providing your own Infrastructure to others, whether free or for a fee, as long as you do not violate these Conditions and AGPLv3 license, including the requirement to publish any modifications of the relay server software.
**Keeping your data secure**. SimpleX Chat is the first communication software that aims to be 100% private by design - server software neither has the ability to access your messages, nor it has information about who you communicate with. That means that you are solely responsible for keeping your device, your user profile and any data safe and secure. If you lose your phone or remove the Software from the device, you will not be able to recover the lost data, unless you made a back up. To protect the data you need to make regular backups, as using old backups may disrupt your communication with some of the contacts.
**Keeping your data secure**. SimpleX Chat is the first communication software that aims to be 100% private by design - server software neither has the ability to access your messages, nor it has information about who you communicate with. That means that you are solely responsible for keeping your device, your user profile and any data safe and secure. If you lose your phone or remove the Software from the device, you will not be able to recover the lost data, unless you made a back up. To protect the data you need to make regular backups, as using old backups may disrupt your communication with some of the contacts. SimpleX Chat Ltd and other preset server operators are not responsible for any data loss.
**Storing the messages on the device**. The messages are stored in the encrypted database on your device. Whether and how database passphrase is stored is determined by the configuration of the Software you use. The databases created prior to 2023 or in CLI (terminal) app may remain unencrypted, and it will be indicated in the app interface. In this case, if you make a backup of the data and store it unencrypted, the backup provider may be able to access the messages. Please note, that the desktop apps can be configured to store the database passphrase in the configuration file in plaintext, and unless you set the passphrase when first running the app, a random passphrase will be used and stored on the device. You can remove it from the device via the app settings.
**Storing the files on the device**. The files currently sent and received in the apps by default (except CLI app) are stored on your device encrypted using unique keys, different for each file, that are stored in the database. Once the message that the file was attached to is removed, even if the copy of the encrypted file is retained, it should be impossible to recover the key allowing to decrypt the file. This local file encryption may affect app performance, and it can be disabled via the app settings. This change will only affect the new files. If you later re-enable the encryption, it will also affect only the new files. If you make a backup of the app data and store it unencrypted, the backup provider will be able to access any unencrypted files. In any case, irrespective of the storage setting, the files are always sent by all apps end-to-end encrypted.
**No Access to Emergency Services**. Our Applications do not provide access to emergency service providers like the police, fire department, hospitals, or other public safety organizations. Make sure you can contact emergency service providers through a mobile, fixed-line telephone, or other service.
**No Access to Emergency Services**. SimpleX Chat Applications do not provide access to emergency service providers like the police, fire department, hospitals, or other public safety organizations. Make sure you can contact emergency service providers through a mobile, fixed-line telephone, or other service.
**Third-party services**. Our Applications may allow you to access, use, or interact with our or third-party websites, apps, content, and other products and services. When you use third-party services, their terms and privacy policies govern your use of those services.
**Third-party services**. SimpleX Chat Applications may allow you to access, use, or interact with the websites of SimpleX Chat Ltd, preset server operators or other third-party websites, apps, content, and other products and services. When you use third-party services, their terms and privacy policies govern your use of those services.
**Your Rights**. You own the messages and the information you transmit through our Applications. Your recipients are able to retain the messages they receive from you; there is no technical ability to delete data from their devices. While there are various app features that allow deleting messages from the recipients' devices, such as _disappearing messages_ and _full message deletion_, their functioning on your recipients' devices cannot be guaranteed or enforced, as the device may be offline or have a modified version of the Software. At the same time, repudiation property of the end-to-end encryption algorithm allows you to plausibly deny having sent the message, like you can deny what you said in a private face-to-face conversation, as the recipient cannot provide any proof to the third parties, by design.
**Your Rights**. You own the messages and the information you transmit through SimpleX Applications. Your recipients are able to retain the messages they receive from you; there is no technical ability to delete data from their devices. While there are various app features that allow deleting messages from the recipients' devices, such as _disappearing messages_ and _full message deletion_, their functioning on your recipients' devices cannot be guaranteed or enforced, as the device may be offline or have a modified version of the Software. At the same time, repudiation property of the end-to-end encryption algorithm allows you to plausibly deny having sent the message, like you can deny what you said in a private face-to-face conversation, as the recipient cannot provide any proof to the third parties, by design.
**License**. SimpleX Chat Ltd grants you a limited, revocable, non-exclusive, and non-transferable license to use our Applications in accordance with these Conditions. The source-code of Applications is available and can be used under [AGPL v3 license](https://github.com/simplex-chat/simplex-chat/blob/stable/LICENSE).
**License**. SimpleX Chat Ltd grants you a limited, revocable, non-exclusive, and non-transferable license to use SimpleX Chat Applications in accordance with these Conditions. The source-code of Applications is available and can be used under [AGPL v3 license](https://github.com/simplex-chat/simplex-chat/blob/stable/LICENSE).
**SimpleX Chat Ltd Rights**. We own all copyrights, trademarks, domains, logos, trade secrets, and other intellectual property rights associated with our Applications. You may not use our copyrights, trademarks, domains, logos, and other intellectual property rights unless you have our written permission, and unless under an open-source license distributed together with the source code. To report copyright, trademark, or other intellectual property infringement, please contact chat@simplex.chat.
**SimpleX Chat Ltd Rights**. SimpleX Chat Ltd (and, where applicable, preset server operators) owns all copyrights, trademarks, domains, logos, trade secrets, and other intellectual property rights associated with the Applications. You may not use SimpleX Chat Ltd copyrights, trademarks, domains, logos, and other intellectual property rights unless you have SimpleX Chat Ltd written permission, and unless under an open-source license distributed together with the source code. To report copyright, trademark, or other intellectual property infringement, please contact chat@simplex.chat.
**Disclaimers**. YOU USE OUR APPLICATIONS AT YOUR OWN RISK AND SUBJECT TO THE FOLLOWING DISCLAIMERS. WE PROVIDE OUR APPLICATIONS ON AN “AS IS” BASIS WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE, NON-INFRINGEMENT, AND FREEDOM FROM COMPUTER VIRUS OR OTHER HARMFUL CODE. SIMPLEX CHAT LTD DOES NOT WARRANT THAT ANY INFORMATION PROVIDED BY US IS ACCURATE, COMPLETE, OR USEFUL, THAT OUR APPLICATIONS WILL BE OPERATIONAL, ERROR-FREE, SECURE, OR SAFE, OR THAT OUR APPLICATIONS WILL FUNCTION WITHOUT DISRUPTIONS, DELAYS, OR IMPERFECTIONS. WE DO NOT CONTROL, AND ARE NOT RESPONSIBLE FOR, CONTROLLING HOW OR WHEN OUR USERS USE OUR APPLICATIONS. WE ARE NOT RESPONSIBLE FOR THE ACTIONS OR INFORMATION (INCLUDING CONTENT) OF OUR USERS OR OTHER THIRD PARTIES. YOU RELEASE US, AFFILIATES, DIRECTORS, OFFICERS, EMPLOYEES, PARTNERS, AND AGENTS ("SIMPLEX PARTIES") FROM ANY CLAIM, COMPLAINT, CAUSE OF ACTION, CONTROVERSY, OR DISPUTE (TOGETHER, "CLAIM") AND DAMAGES, KNOWN AND UNKNOWN, RELATING TO, ARISING OUT OF, OR IN ANY WAY CONNECTED WITH ANY SUCH CLAIM YOU HAVE AGAINST ANY THIRD PARTIES.
**Disclaimers**. YOU USE SIMPLEX APPLICATIONS AT YOUR OWN RISK AND SUBJECT TO THE FOLLOWING DISCLAIMERS. SIMPLEX CHAT LTD PROVIDES APPLICATIONS ON AN “AS IS” BASIS WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE, NON-INFRINGEMENT, AND FREEDOM FROM COMPUTER VIRUS OR OTHER HARMFUL CODE. SIMPLEX CHAT LTD DOES NOT WARRANT THAT ANY INFORMATION PROVIDED BY THEM IS ACCURATE, COMPLETE, OR USEFUL, THAT THEIR APPLICATIONS WILL BE OPERATIONAL, ERROR-FREE, SECURE, OR SAFE, OR THAT THEIR APPLICATIONS WILL FUNCTION WITHOUT DISRUPTIONS, DELAYS, OR IMPERFECTIONS. SIMPLEX CHAT LTD AND OTHER PRESET OPERATORS DO NOT CONTROL, AND ARE NOT RESPONSIBLE FOR, CONTROLLING HOW OR WHEN THE USERS USE APPLICATIONS. SIMPLEX CHAT LTD AND OTHER PRESET OPERATORS ARE NOT RESPONSIBLE FOR THE ACTIONS OR INFORMATION (INCLUDING CONTENT) OF THEIR USERS OR OTHER THIRD PARTIES. YOU RELEASE SIMPLEX CHAT LTD, OTHER PRESET OPERATORS, AFFILIATES, DIRECTORS, OFFICERS, EMPLOYEES, PARTNERS, AND AGENTS ("SIMPLEX PARTIES") FROM ANY CLAIM, COMPLAINT, CAUSE OF ACTION, CONTROVERSY, OR DISPUTE (TOGETHER, "CLAIM") AND DAMAGES, KNOWN AND UNKNOWN, RELATING TO, ARISING OUT OF, OR IN ANY WAY CONNECTED WITH ANY SUCH CLAIM YOU HAVE AGAINST ANY THIRD PARTIES.
**Limitation of liability**. THE SIMPLEX PARTIES WILL NOT BE LIABLE TO YOU FOR ANY LOST PROFITS OR CONSEQUENTIAL, SPECIAL, PUNITIVE, INDIRECT, OR INCIDENTAL DAMAGES RELATING TO, ARISING OUT OF, OR IN ANY WAY IN CONNECTION WITH OUR CONDITIONS, US, OR OUR APPLICATIONS, EVEN IF THE SIMPLEX PARTIES HAVE BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. OUR AGGREGATE LIABILITY RELATING TO, ARISING OUT OF, OR IN ANY WAY IN CONNECTION WITH OUR CONDITIONS, US, OR OUR APPLICATIONS WILL NOT EXCEED ONE DOLLAR ($1). THE FOREGOING DISCLAIMER OF CERTAIN DAMAGES AND LIMITATION OF LIABILITY WILL APPLY TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW. THE LAWS OF SOME JURISDICTIONS MAY NOT ALLOW THE EXCLUSION OR LIMITATION OF CERTAIN DAMAGES, SO SOME OR ALL OF THE EXCLUSIONS AND LIMITATIONS SET FORTH ABOVE MAY NOT APPLY TO YOU. NOTWITHSTANDING ANYTHING TO THE CONTRARY IN OUR CONDITIONS, IN SUCH CASES, THE LIABILITY OF THE SIMPLEX PARTIES WILL BE LIMITED TO THE EXTENT PERMITTED BY APPLICABLE LAW.
**Limitation of liability**. THE SIMPLEX PARTIES WILL NOT BE LIABLE TO YOU FOR ANY LOST PROFITS OR CONSEQUENTIAL, SPECIAL, PUNITIVE, INDIRECT, OR INCIDENTAL DAMAGES RELATING TO, ARISING OUT OF, OR IN ANY WAY IN CONNECTION WITH OUR CONDITIONS, US, OR SIMPLEX APPLICATIONS, EVEN IF THE SIMPLEX PARTIES HAVE BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. THE AGGREGATE LIABILITY OF THE SIMPLEX PARTIES RELATING TO, ARISING OUT OF, OR IN ANY WAY IN CONNECTION WITH THESE CONDITIONS, THE SIMPLEX PARTIES, OR THE APPLICATIONS WILL NOT EXCEED ONE DOLLAR ($1). THE FOREGOING DISCLAIMER OF CERTAIN DAMAGES AND LIMITATION OF LIABILITY WILL APPLY TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW. THE LAWS OF SOME JURISDICTIONS MAY NOT ALLOW THE EXCLUSION OR LIMITATION OF CERTAIN DAMAGES, SO SOME OR ALL OF THE EXCLUSIONS AND LIMITATIONS SET FORTH ABOVE MAY NOT APPLY TO YOU. NOTWITHSTANDING ANYTHING TO THE CONTRARY IN THE CONDITIONS, IN SUCH CASES, THE LIABILITY OF THE SIMPLEX PARTIES WILL BE LIMITED TO THE EXTENT PERMITTED BY APPLICABLE LAW.
**Availability**. Our Applications may be disrupted, including for maintenance, upgrades, or network or equipment failures. We may discontinue some or all of our Applications, including certain features and the support for certain devices and platforms, at any time.
**Availability**. The Applications may be disrupted, including for maintenance, upgrades, or network or equipment failures. SimpleX Chat Ltd may discontinue some or all of their Applications, including certain features and the support for certain devices and platforms, at any time. Preset server operators may discontinue providing the servers, at any time.
**Resolving disputes**. You agree to resolve any Claim you have with us relating to or arising from our Conditions, us, or our Applications in the courts of England and Wales. You also agree to submit to the personal jurisdiction of such courts for the purpose of resolving all such disputes. The laws of England govern our Conditions, as well as any disputes, whether in court or arbitration, which might arise between SimpleX Chat Ltd and you, without regard to conflict of law provisions.
**Resolving disputes**. You agree to resolve any Claim you have with SimpleX Chat Ltd and/or preset server operators relating to or arising from these Conditions, them, or the Applications in the courts of England and Wales. You also agree to submit to the personal jurisdiction of such courts for the purpose of resolving all such disputes. The laws of England govern these Conditions, as well as any disputes, whether in court or arbitration, which might arise between SimpleX Chat Ltd (or preset server operators) and you, without regard to conflict of law provisions.
**Changes to the conditions**. SimpleX Chat Ltd may update the Conditions from time to time. Your continued use of our Applications confirms your acceptance of our updated Conditions and supersedes any prior Conditions. You will comply with all applicable export control and trade sanctions laws. Our Conditions cover the entire agreement between you and SimpleX Chat Ltd regarding our Applications. If you do not agree with our Conditions, you should stop using our Applications.
**Changes to the conditions**. SimpleX Chat Ltd may update the Conditions from time to time. The updated conditions have to be accepted within 30 days. Even if you fail to accept updated conditions, your continued use of SimpleX Chat Applications confirms your acceptance of the updated Conditions and supersedes any prior Conditions. You will comply with all applicable export control and trade sanctions laws. These Conditions cover the entire agreement between you and SimpleX Chat Ltd, and any preset server operators where applicable, regarding SimpleX Chat Applications. If you do not agree with these Conditions, you should stop using the Applications.
**Enforcing the conditions**. If we fail to enforce any of our Conditions, that does not mean we waive the right to enforce them. If any provision of the Conditions is deemed unlawful, void, or unenforceable, that provision shall be deemed severable from our Conditions and shall not affect the enforceability of the remaining provisions. Our Applications are not intended for distribution to or use in any country where such distribution or use would violate local law or would subject us to any regulations in another country. We reserve the right to limit our Applications in any country. If you have specific questions about these Conditions, please contact us at chat@simplex.chat.
**Enforcing the conditions**. If SimpleX Chat Ltd or preset server operators fail to enforce any of these Conditions, that does not mean they waive the right to enforce them. If any provision of the Conditions is deemed unlawful, void, or unenforceable, that provision shall be deemed severable from the Conditions and shall not affect the enforceability of the remaining provisions. The Applications are not intended for distribution to or use in any country where such distribution or use would violate local law or would subject SimpleX Chat Ltd to any regulations in another country. SimpleX Chat Ltd reserve the right to limit the access to the Applications in any country. Preset operators reserve the right to limit access to their servers in any country. If you have specific questions about these Conditions, please contact SimpleX Chat Ltd at chat@simplex.chat.
**Ending these conditions**. You may end these Conditions with SimpleX Chat Ltd at any time by deleting our Applications from your devices and discontinuing use of our Infrastructure. The provisions related to Licenses, Disclaimers, Limitation of Liability, Resolving dispute, Availability, Changes to the conditions, Enforcing the conditions, and Ending these conditions will survive termination of your relationship with SimpleX Chat Ltd.
**Ending these conditions**. You may end these Conditions with SimpleX Chat Ltd and preset server operators at any time by deleting the Applications from your devices and discontinuing use of the Infrastructure of SimpleX Chat Ltd and preset server operators. The provisions related to Licenses, Disclaimers, Limitation of Liability, Resolving dispute, Availability, Changes to the conditions, Enforcing the conditions, and Ending these conditions will survive termination of your relationship with SimpleX Chat Ltd and/or preset server operators.
Updated April 24, 2024
Updated November 14, 2024
+12 -18
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 users group](https://simplex.chat/contact#/?v=1-4&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2Fos8FftfoV8zjb2T89fUEjJtF7y64p5av%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAQqMgh0fw2lPhjn3PDIEfAKA_E0-gf8Hr8zzhYnDivRs%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22lBPiveK2mjfUH43SN77R0w%3D%3D%22%7D)
You can join an English-speaking users group if you want to ask any questions: [#SimpleX users group](https://simplex.chat/contact#/?v=2-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-4&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FvYCRjIflKNMGYlfTkuHe4B40qSlQ0439%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAHNdcqNbzXZhyMoSBjT2R0-Eb1EPaLyUg3KZjn-kmM1w%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22PD20tcXjw7IpkkMCfR6HLA%3D%3D%22%7D) for developers who build on SimpleX platform:
@@ -83,7 +83,7 @@ There is also a group [#simplex-devs](https://simplex.chat/contact#/?v=1-4&smp=s
There are groups in other languages, that we have the apps interface translated into. These groups are for testing, and asking questions to other SimpleX Chat users:
[\#SimpleX-DE](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FkIEl7OQzcp-J6aDmjdlQbRJwqkcZE7XR%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAR16PCu02MobRmKAsjzhDWMZcWP9hS8l5AUZi-Gs8z18%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22puYPMCQt11yPUvgmI5jCiw%3D%3D%22%7D) (German-speaking), [\#SimpleX-ES](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FaJ8O1O8A8GbeoaHTo_V8dcefaCl7ouPb%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEA034qWTA3sWcTsi6aWhNf9BA34vKVCFaEBdP2R66z6Ao%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22wiZ1v_wNjLPlT-nCSB-bRA%3D%3D%22%7D) (Spanish-speaking), [\#SimpleX-FR](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2Fhpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg%3D%40smp5.simplex.im%2FvIHQDxTor53nwnWWTy5cHNwQQAdWN5Hw%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAPdgK1eBnETmgiqEQufbUkydKBJafoRx4iRrtrC2NAGc%253D%26srv%3Djjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%221FyUryBPza-1ZFFE80Ekbg%3D%3D%22%7D) (French-speaking), [\#SimpleX-RU](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FXZyt3hJmWsycpN7Dqve_wbrAqb6myk1R%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAMFVIoytozTEa_QXOgoZFq_oe0IwZBYKvW50trSFXzXo%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22xz05ngjA3pNIxLZ32a8Vxg%3D%3D%22%7D) (Russian-speaking), [\#SimpleX-IT](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2F0weR-ZgDUl7ruOtI_8TZwEsnJP6UiImA%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAq4PSThO9Fvb5ydF48wB0yNbpzCbuQJCW3vZ9BGUfcxk%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22e-iceLA0SctC62eARgYDWg%3D%3D%22%7D) (Italian-speaking).
[\#SimpleX-DE](https://simplex.chat/contact#/?v=1-4&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FmfiivxDKWFuowXrQOp11jsY8TuP__rBL%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAiz3pKNwvKudckFYMUfgoT0s96B0jfZ7ALHAu7rtE9HQ%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22jZeJpXGrRXQJU_-MSJ_v2A%3D%3D%22%7D) (German-speaking), [\#SimpleX-ES](https://simplex.chat/contact#/?v=2-4&smp=smp%3A%2F%2Fhpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg%3D%40smp5.simplex.im%2FJ5ES83pJimY2BRklS8fvy_iQwIU37xra%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEA0F0STP6UqN_12_k2cjjTrIjFgBGeWhOAmbY1qlk3pnM%253D%26srv%3Djjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22VmUU0fqmYdCRmVCyvStvHA%3D%3D%22%7D) (Spanish-speaking), [\#SimpleX-FR](https://simplex.chat/contact#/?v=2-7&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FxCHBE_6PBRMqNEpm4UQDHXb9cz-mN7dd%23%2F%3Fv%3D1-3%26dh%3DMCowBQYDK2VuAyEAetqlcM7zTCRw-iatnwCrvpJSto7lq5Yv6AsBMWv7GSM%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22foO5Xw4hhjOa_x7zET7otw%3D%3D%22%7D) (French-speaking), [\#SimpleX-RU](https://simplex.chat/contact#/?v=2-4&smp=smp%3A%2F%2Fhpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg%3D%40smp5.simplex.im%2FVXQTB0J2lLjYkgjWByhl6-1qmb5fgZHh%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAI6JaEWezfSwvcoTEkk6au-gkjrXR2ew2OqZYMYBvayk%253D%26srv%3Djjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22ORH9OEe8Duissh-hslfeVg%3D%3D%22%7D) (Russian-speaking), [\#SimpleX-IT](https://simplex.chat/contact#/?v=2-7&smp=smp%3A%2F%2Fhpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg%3D%40smp5.simplex.im%2FqpHu0psOUdYfc11yQCzSyq5JhijrBzZT%23%2F%3Fv%3D1-3%26dh%3DMCowBQYDK2VuAyEACZ_7fbwlM45wl6cGif8cY47oPQ_AMdP0ATqOYLA6zHY%253D%26srv%3Djjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%229uRQRTir3ealdcSfB0zsrw%3D%3D%22%7D) (Italian-speaking).
You can join either by opening these links in the app or by opening them in a desktop browser and scanning the QR code.
@@ -233,34 +233,24 @@ You can use SimpleX with your own servers and still communicate with people usin
Recent and important updates:
[Dec 10, 2024. SimpleX network: preset servers operated by Flux, business chats and more with v6.2 of the apps](./20241210-simplex-network-v6-2-servers-by-flux-business-chats.md)
[Oct 14, 2024. SimpleX network: security review of protocols design by Trail of Bits, v6.1 released with better calls and user experience.](./blog/20241014-simplex-network-v6-1-security-review-better-calls-user-experience.md)
[Aug 14, 2024. SimpleX network: the investment from Jack Dorsey and Asymmetric, v6.0 released with the new user experience and private message routing](./blog/20240814-simplex-chat-vision-funding-v6-private-routing-new-user-experience.md)
[Jun 4, 2024. SimpleX network: private message routing, v5.8 released with IP address protection and chat themes](./blog/20240604-simplex-chat-v5.8-private-message-routing-chat-themes.md)
[Apr 26, 2024. SimpleX network: legally binding transparency, v5.7 released with better calls and messages.](./blog/20240426-simplex-legally-binding-transparency-v5-7-better-user-experience.md)
[Mar 23, 2024. SimpleX network: real privacy and stable profits, non-profits for protocols, v5.6 released with quantum resistant e2e encryption and simple profile migration.](./blog/20240323-simplex-network-privacy-non-profit-v5-6-quantum-resistant-e2e-encryption-simple-migration.md)
[Mar 14, 2024. SimpleX Chat v5.6 beta: adding quantum resistance to Signal double ratchet algorithm.](./blog/20240314-simplex-chat-v5-6-quantum-resistance-signal-double-ratchet-algorithm.md)
[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).
[Jul 22, 2023. SimpleX Chat: v5.2 released with message delivery receipts](./blog/20230722-simplex-chat-v5-2-message-delivery-receipts.md).
[May 23, 2023. SimpleX Chat: v5.1 released with message reactions and self-destruct passcode](./blog/20230523-simplex-chat-v5-1-message-reactions-self-destruct-passcode.md).
[Apr 22, 2023. SimpleX Chat: vision and funding, v5.0 released with videos and files up to 1gb](./blog/20230422-simplex-chat-vision-funding-v5-videos-files-passcode.md).
[Mar 1, 2023. SimpleX File Transfer Protocol send large files efficiently, privately and securely, soon to be integrated into SimpleX Chat apps.](./blog/20230301-simplex-file-transfer-protocol.md).
[Nov 8, 2022. Security audit by Trail of Bits, the new website and v4.2 released](./blog/20221108-simplex-chat-v4.2-security-audit-new-website.md).
[Sep 28, 2022. v4.0: encrypted local chat database and many other changes](./blog/20220928-simplex-chat-v4-encrypted-database.md).
[All updates](./blog)
## :zap: Quick installation of a terminal app
@@ -388,9 +378,11 @@ Please also join [#simplex-devs](https://simplex.chat/contact#/?v=1-2&smp=smp%3A
- ✅ Improve sending videos (including encryption of locally stored videos).
- ✅ Post-quantum resistant key exchange in double ratchet protocol.
- ✅ Message delivery relay for senders (to conceal IP address from the recipients' servers and to reduce the traffic).
- ✅ Support multiple network operators in the app.
- 🏗 Large groups, communities and public channels.
- 🏗 Short links to connect and join groups.
- 🏗 Improve stability and reduce battery usage.
- 🏗 Improve experience for the new users.
- 🏗 Large groups, communities and public channels.
- Privacy & security slider - a simple way to set all settings at once.
- SMP queue redundancy and rotation (manual is supported).
- Include optional message into connection request sent via contact address.
@@ -409,7 +401,9 @@ Please also join [#simplex-devs](https://simplex.chat/contact#/?v=1-2&smp=smp%3A
[SimpleX protocols and security model](https://github.com/simplex-chat/simplexmq/blob/master/protocol/overview-tjr.md) was reviewed, and had many breaking changes and improvements in v1.0.0.
The security audit was performed in October 2022 by [Trail of Bits](https://www.trailofbits.com/about), and most fixes were released in v4.2.0 see [the announcement](./blog/20221108-simplex-chat-v4.2-security-audit-new-website.md).
The implementation security assessment of SimpleX cryptography and networking was done in October 2022 by [Trail of Bits](https://www.trailofbits.com/about) see [the announcement](./blog/20221108-simplex-chat-v4.2-security-audit-new-website.md).
The cryptographic review of SimpleX protocols was done in July 2024 by Trail of Bits see [the announcement](./blog/20241014-simplex-network-v6-1-security-review-better-calls-user-experience.md).
SimpleX Chat is still a relatively early stage platform (the mobile apps were released in March 2022), so you may discover some bugs and missing features. We would really appreciate if you let us know anything that needs to be fixed or improved.
+1 -5
View File
@@ -15,16 +15,12 @@ class AppDelegate: NSObject, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
logger.debug("AppDelegate: didFinishLaunchingWithOptions")
application.registerForRemoteNotifications()
NotificationCenter.default.addObserver(self, selector: #selector(pasteboardChanged), name: UIPasteboard.changedNotification, object: nil)
removePasscodesIfReinstalled()
prepareForLaunch()
deleteOldChatArchive()
return true
}
@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)")
@@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "Flux_logo_blue_white.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

@@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "Flux_logo_blue.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

@@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "Flux_symbol_blue-white.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

+65 -14
View File
@@ -9,10 +9,23 @@ import SwiftUI
import Intents
import SimpleXChat
private enum NoticesSheet: Identifiable {
case whatsNew(updatedConditions: Bool)
case updatedConditions
var id: String {
switch self {
case .whatsNew: return "whatsNew"
case .updatedConditions: return "updatedConditions"
}
}
}
struct ContentView: View {
@EnvironmentObject var chatModel: ChatModel
@ObservedObject var alertManager = AlertManager.shared
@ObservedObject var callController = CallController.shared
@ObservedObject var appSheetState = AppSheetState.shared
@Environment(\.colorScheme) var colorScheme
@EnvironmentObject var theme: AppTheme
@EnvironmentObject var sceneDelegate: SceneDelegate
@@ -29,12 +42,13 @@ struct ContentView: View {
@AppStorage(DEFAULT_PERFORM_LA) private var prefPerformLA = false
@AppStorage(DEFAULT_PRIVACY_PROTECT_SCREEN) private var protectScreen = false
@AppStorage(DEFAULT_NOTIFICATION_ALERT_SHOWN) private var notificationAlertShown = false
@State private var showSettings = false
@State private var showWhatsNew = false
@State private var noticesShown = false
@State private var noticesSheetItem: NoticesSheet? = nil
@State private var showChooseLAMode = false
@State private var showSetPasscode = false
@State private var waitingForOrPassedAuth = true
@State private var chatListActionSheet: ChatListActionSheet? = nil
@State private var chatListUserPickerSheet: UserPickerSheet? = nil
private let callTopPadding: CGFloat = 40
@@ -86,7 +100,7 @@ struct ContentView: View {
callView(call)
}
if !showSettings, let la = chatModel.laRequest {
if chatListUserPickerSheet == nil, let la = chatModel.laRequest {
LocalAuthView(authRequest: la)
.onDisappear {
// this flag is separate from accessAuthenticated to show initializationView while we wait for authentication
@@ -109,9 +123,6 @@ struct ContentView: View {
}
}
.alert(isPresented: $alertManager.presentAlert) { alertManager.alertView! }
.sheet(isPresented: $showSettings) {
SettingsView(showSettings: $showSettings)
}
.confirmationDialog("SimpleX Lock mode", isPresented: $showChooseLAMode, titleVisibility: .visible) {
Button("System authentication") { initialEnableLA() }
Button("Passcode entry") { showSetPasscode = true }
@@ -253,7 +264,8 @@ struct ContentView: View {
private func mainView() -> some View {
ZStack(alignment: .top) {
ChatListView(showSettings: $showSettings).privacySensitive(protectScreen)
ChatListView(activeUserPickerSheet: $chatListUserPickerSheet)
.redacted(reason: appSheetState.redactionReasons(protectScreen))
.onAppear {
requestNtfAuthorization()
// Local Authentication notice is to be shown on next start after onboarding is complete
@@ -262,8 +274,15 @@ struct ContentView: View {
alertManager.showAlert(laNoticeAlert())
} else if !chatModel.showCallView && CallController.shared.activeCallInvitation == nil {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
if !showWhatsNew {
showWhatsNew = shouldShowWhatsNew()
if !noticesShown {
let showWhatsNew = shouldShowWhatsNew()
let showUpdatedConditions = chatModel.conditions.conditionsAction?.showNotice ?? false
noticesShown = showWhatsNew || showUpdatedConditions
if showWhatsNew {
noticesSheetItem = .whatsNew(updatedConditions: showUpdatedConditions)
} else if showUpdatedConditions {
noticesSheetItem = .updatedConditions
}
}
}
}
@@ -271,8 +290,22 @@ struct ContentView: View {
connectViaUrl()
}
.onChange(of: chatModel.appOpenUrl) { _ in connectViaUrl() }
.sheet(isPresented: $showWhatsNew) {
WhatsNewView()
.sheet(item: $noticesSheetItem) { item in
switch item {
case let .whatsNew(updatedConditions):
WhatsNewView(updatedConditions: updatedConditions)
.modifier(ThemedBackground())
.if(updatedConditions) { v in
v.task { await setConditionsNotified_() }
}
case .updatedConditions:
UsageConditionsView(
currUserServers: Binding.constant([]),
userServers: Binding.constant([])
)
.modifier(ThemedBackground(grouped: true))
.task { await setConditionsNotified_() }
}
}
if chatModel.setDeliveryReceipts {
SetDeliveryReceiptsView()
@@ -284,6 +317,15 @@ struct ContentView: View {
.onContinueUserActivity("INStartVideoCallIntent", perform: processUserActivity)
}
private func setConditionsNotified_() async {
do {
let conditionsId = ChatModel.shared.conditions.currentConditions.conditionsId
try await setConditionsNotified(conditionsId: conditionsId)
} catch let error {
logger.error("setConditionsNotified error: \(responseError(error))")
}
}
private func processUserActivity(_ activity: NSUserActivity) {
let intent = activity.interaction?.intent
if let intent = intent as? INStartCallIntent {
@@ -300,9 +342,18 @@ struct ContentView: View {
if let contactId = contacts?.first?.personHandle?.value,
let chat = chatModel.getChat(contactId),
case let .direct(contact) = chat.chatInfo {
logger.debug("callToRecentContact: schedule call")
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
CallController.shared.startCall(contact, mediaType)
let activeCall = chatModel.activeCall
// This line works when a user clicks on a video button in CallKit UI while in call.
// The app tries to make another call to the same contact and overwite activeCall instance making its state broken
if let activeCall, contactId == activeCall.contact.id, mediaType == .video, !activeCall.hasVideo {
Task {
await chatModel.callCommand.processCommand(.media(source: .camera, enable: true))
}
} else if activeCall == nil {
logger.debug("callToRecentContact: schedule call")
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
CallController.shared.startCall(contact, mediaType)
}
}
}
}
+24 -17
View File
@@ -61,7 +61,7 @@ class ItemsModel: ObservableObject {
init() {
publisher
.throttle(for: 0.25, scheduler: DispatchQueue.main, latest: true)
.throttle(for: 0.2, scheduler: DispatchQueue.main, latest: true)
.sink { self.objectWillChange.send() }
.store(in: &bag)
}
@@ -147,6 +147,7 @@ final class ChatModel: ObservableObject {
@Published var chatDbEncrypted: Bool?
@Published var chatDbStatus: DBMigrationResult?
@Published var ctrlInitInProgress: Bool = false
@Published var notificationResponse: UNNotificationResponse?
// local authentication
@Published var contentViewAccessAuthenticated: Bool = false
@Published var laRequest: LocalAuthRequest?
@@ -191,8 +192,9 @@ final class ChatModel: ObservableObject {
@Published var stopPreviousRecPlay: URL? = nil // coordinates currently playing source
@Published var draft: ComposeState?
@Published var draftChatId: String?
@Published var pasteboardHasStrings: Bool = UIPasteboard.general.hasStrings
@Published var networkInfo = UserNetworkInfo(networkType: .other, online: true)
// usage conditions
@Published var conditions: ServerOperatorConditions = .empty
var messageDelivery: Dictionary<Int64, () -> Void> = [:]
@@ -527,7 +529,7 @@ final class ChatModel: ObservableObject {
}
func updateCurrentUserUiThemes(uiThemes: ThemeModeOverrides?) {
guard var current = currentUser else { return }
guard var current = currentUser, current.uiThemes != uiThemes else { return }
current.uiThemes = uiThemes
let i = users.firstIndex(where: { $0.user.userId == current.userId })
if let i {
@@ -563,6 +565,7 @@ final class ChatModel: ObservableObject {
// update preview
_updateChat(cInfo.id) { chat in
self.decreaseUnreadCounter(user: self.currentUser!, by: chat.chatStats.unreadCount)
self.updateFloatingButtons(unreadCount: 0)
chat.chatStats = ChatStats()
}
// update current chat
@@ -579,6 +582,12 @@ final class ChatModel: ObservableObject {
}
}
private func updateFloatingButtons(unreadCount: Int) {
let fbm = ChatView.FloatingButtonModel.shared
fbm.totalUnread = unreadCount
fbm.objectWillChange.send()
}
func markChatItemsRead(_ cInfo: ChatInfo, aboveItem: ChatItem? = nil) {
if let cItem = aboveItem {
if chatId == cInfo.id, let i = getChatItemIndex(cItem) {
@@ -597,6 +606,7 @@ final class ChatModel: ObservableObject {
if markedCount > 0 {
chat.chatStats.unreadCount -= markedCount
self.decreaseUnreadCounter(user: self.currentUser!, by: markedCount)
self.updateFloatingButtons(unreadCount: chat.chatStats.unreadCount)
}
}
}
@@ -626,19 +636,15 @@ final class ChatModel: ObservableObject {
}
}
func markChatItemRead(_ cInfo: ChatInfo, _ cItem: ChatItem) async {
if chatId == cInfo.id,
let itemIndex = getChatItemIndex(cItem),
im.reversedChatItems[itemIndex].isRcvNew {
await MainActor.run {
withTransaction(Transaction()) {
// update current chat
markChatItemRead_(itemIndex)
// update preview
unreadCollector.changeUnreadCounter(cInfo.id, by: -1)
func markChatItemsRead(_ cInfo: ChatInfo, _ itemIds: [ChatItem.ID]) {
if self.chatId == cInfo.id {
for itemId in itemIds {
if let i = im.reversedChatItems.firstIndex(where: { $0.id == itemId }) {
markChatItemRead_(i)
}
}
}
self.unreadCollector.changeUnreadCounter(cInfo.id, by: -itemIds.count)
}
private let unreadCollector = UnreadCollector()
@@ -664,9 +670,10 @@ final class ChatModel: ObservableObject {
}
func changeUnreadCounter(_ chatId: ChatId, by count: Int) {
DispatchQueue.main.async {
self.unreadCounts[chatId] = (self.unreadCounts[chatId] ?? 0) + count
if chatId == ChatModel.shared.chatId {
ChatView.FloatingButtonModel.shared.totalUnread += count
}
self.unreadCounts[chatId] = (self.unreadCounts[chatId] ?? 0) + count
subject.send()
}
}
@@ -838,7 +845,7 @@ final class ChatModel: ObservableObject {
}
func dismissConnReqView(_ id: String) {
if id == showingInvitation?.connId {
if id == showingInvitation?.pcc.id {
markShowingInvitationUsed()
dismissAllSheets()
}
@@ -891,7 +898,7 @@ final class ChatModel: ObservableObject {
}
struct ShowingInvitation {
var connId: String
var pcc: PendingContactConnection
var connChatUsed: Bool
}
+34 -11
View File
@@ -26,20 +26,37 @@ enum NtfCallAction {
class NtfManager: NSObject, UNUserNotificationCenterDelegate, ObservableObject {
static let shared = NtfManager()
public var navigatingToChat = false
private var granted = false
private var prevNtfTime: Dictionary<ChatId, Date> = [:]
override init() {
super.init()
UNUserNotificationCenter.current().delegate = self
}
// Handle notification when app is in background
func userNotificationCenter(_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse,
withCompletionHandler handler: () -> Void) {
logger.debug("NtfManager.userNotificationCenter: didReceive")
let content = response.notification.request.content
if appStateGroupDefault.get() == .active {
processNotificationResponse(response)
} else {
logger.debug("NtfManager.userNotificationCenter: remember response in model")
ChatModel.shared.notificationResponse = response
}
handler()
}
func processNotificationResponse(_ ntfResponse: UNNotificationResponse) {
let chatModel = ChatModel.shared
let action = response.actionIdentifier
logger.debug("NtfManager.userNotificationCenter: didReceive: action \(action), categoryIdentifier \(content.categoryIdentifier)")
let content = ntfResponse.notification.request.content
let action = ntfResponse.actionIdentifier
logger.debug("NtfManager.processNotificationResponse: didReceive: action \(action), categoryIdentifier \(content.categoryIdentifier)")
if let userId = content.userInfo["userId"] as? Int64,
userId != chatModel.currentUser?.userId {
logger.debug("NtfManager.processNotificationResponse changeActiveUser")
changeActiveUser(userId, viewPwd: nil)
}
if content.categoryIdentifier == ntfCategoryContactRequest && (action == ntfActionAcceptContact || action == ntfActionAcceptContactIncognito),
@@ -58,10 +75,12 @@ class NtfManager: NSObject, UNUserNotificationCenterDelegate, ObservableObject {
}
} else {
if let chatId = content.targetContentIdentifier {
ItemsModel.shared.loadOpenChat(chatId)
self.navigatingToChat = true
ItemsModel.shared.loadOpenChat(chatId) {
self.navigatingToChat = false
}
}
}
handler()
}
private func ntfCallAction(_ content: UNNotificationContent, _ action: String) -> (ChatId, NtfCallAction)? {
@@ -76,7 +95,6 @@ class NtfManager: NSObject, UNUserNotificationCenterDelegate, ObservableObject {
return nil
}
// Handle notification when the app is in foreground
func userNotificationCenter(_ center: UNUserNotificationCenter,
willPresent notification: UNNotification,
@@ -185,6 +203,12 @@ class NtfManager: NSObject, UNUserNotificationCenterDelegate, ObservableObject {
actions: [],
intentIdentifiers: [],
hiddenPreviewsBodyPlaceholder: NSLocalizedString("SimpleX encrypted message or connection event", comment: "notification")
),
UNNotificationCategory(
identifier: ntfCategoryManyEvents,
actions: [],
intentIdentifiers: [],
hiddenPreviewsBodyPlaceholder: NSLocalizedString("New events", comment: "notification")
)
])
}
@@ -210,29 +234,28 @@ class NtfManager: NSObject, UNUserNotificationCenterDelegate, ObservableObject {
}
}
}
center.delegate = self
}
func notifyContactRequest(_ user: any UserLike, _ contactRequest: UserContactRequest) {
logger.debug("NtfManager.notifyContactRequest")
addNotification(createContactRequestNtf(user, contactRequest))
addNotification(createContactRequestNtf(user, contactRequest, 0))
}
func notifyContactConnected(_ user: any UserLike, _ contact: Contact) {
logger.debug("NtfManager.notifyContactConnected")
addNotification(createContactConnectedNtf(user, contact))
addNotification(createContactConnectedNtf(user, contact, 0))
}
func notifyMessageReceived(_ user: any UserLike, _ cInfo: ChatInfo, _ cItem: ChatItem) {
logger.debug("NtfManager.notifyMessageReceived")
if cInfo.ntfsEnabled {
addNotification(createMessageReceivedNtf(user, cInfo, cItem))
addNotification(createMessageReceivedNtf(user, cInfo, cItem, 0))
}
}
func notifyCallInvitation(_ invitation: RcvCallInvitation) {
logger.debug("NtfManager.notifyCallInvitation")
addNotification(createCallInvitationNtf(invitation))
addNotification(createCallInvitationNtf(invitation, 0))
}
func setNtfBadgeCount(_ count: Int) {
+258 -106
View File
@@ -357,6 +357,12 @@ func apiGetChatItemInfo(type: ChatType, id: Int64, itemId: Int64) async throws -
throw r
}
func apiPlanForwardChatItems(type: ChatType, id: Int64, itemIds: [Int64]) async throws -> ([Int64], ForwardConfirmation?) {
let r = await chatSendCmd(.apiPlanForwardChatItems(toChatType: type, toChatId: id, itemIds: itemIds))
if case let .forwardPlan(_, chatItemIds, forwardConfimation) = r { return (chatItemIds, forwardConfimation) }
throw r
}
func apiForwardChatItems(toChatType: ChatType, toChatId: Int64, fromChatType: ChatType, fromChatId: Int64, itemIds: [Int64], ttl: Int?) async -> [ChatItem]? {
let cmd: ChatCommand = .apiForwardChatItems(toChatType: toChatType, toChatId: toChatId, fromChatType: fromChatType, fromChatId: fromChatId, itemIds: itemIds, ttl: ttl)
return await processSendMessageCmd(toChatType: toChatType, cmd: cmd)
@@ -440,6 +446,13 @@ func apiChatItemReaction(type: ChatType, id: Int64, itemId: Int64, add: Bool, re
throw r
}
func apiGetReactionMembers(groupId: Int64, itemId: Int64, reaction: MsgReaction) async throws -> [MemberReaction] {
let userId = try currentUserId("apiGetReactionMemebers")
let r = await chatSendCmd(.apiGetReactionMembers(userId: userId, groupId: groupId, itemId: itemId, reaction: reaction ))
if case let .reactionMembers(_, memberReactions) = r { return memberReactions }
throw r
}
func apiDeleteChatItems(type: ChatType, id: Int64, itemIds: [Int64], mode: CIDeleteMode) async throws -> [ChatItemDeletion] {
let r = await chatSendCmd(.apiDeleteChatItem(type: type, id: id, itemIds: itemIds, mode: mode), bgDelay: msgDelay)
if case let .chatItemsDeleted(_, items, _) = r { return items }
@@ -456,7 +469,7 @@ func apiGetNtfToken() -> (DeviceToken?, NtfTknStatus?, NotificationsMode, String
let r = chatSendCmdSync(.apiGetNtfToken)
switch r {
case let .ntfToken(token, status, ntfMode, ntfServer): return (token, status, ntfMode, ntfServer)
case .chatCmdError(_, .errorAgent(.CMD(.PROHIBITED))): return (nil, nil, .off, nil)
case .chatCmdError(_, .errorAgent(.CMD(.PROHIBITED, _))): return (nil, nil, .off, nil)
default:
logger.debug("apiGetNtfToken response: \(String(describing: r))")
return (nil, nil, .off, nil)
@@ -494,18 +507,6 @@ func apiDeleteToken(token: DeviceToken) async throws {
try await sendCommandOkResp(.apiDeleteToken(token: token))
}
func getUserProtoServers(_ serverProtocol: ServerProtocol) throws -> UserProtoServers {
let userId = try currentUserId("getUserProtoServers")
let r = chatSendCmdSync(.apiGetUserProtoServers(userId: userId, serverProtocol: serverProtocol))
if case let .userProtoServers(_, servers) = r { return servers }
throw r
}
func setUserProtoServers(_ serverProtocol: ServerProtocol, servers: [ServerCfg]) async throws {
let userId = try currentUserId("setUserProtoServers")
try await sendCommandOkResp(.apiSetUserProtoServers(userId: userId, serverProtocol: serverProtocol, servers: servers))
}
func testProtoServer(server: String) async throws -> Result<(), ProtocolTestFailure> {
let userId = try currentUserId("testProtoServer")
let r = await chatSendCmd(.apiTestProtoServer(userId: userId, server: server))
@@ -518,6 +519,72 @@ func testProtoServer(server: String) async throws -> Result<(), ProtocolTestFail
throw r
}
func getServerOperators() async throws -> ServerOperatorConditions {
let r = await chatSendCmd(.apiGetServerOperators)
if case let .serverOperatorConditions(conditions) = r { return conditions }
logger.error("getServerOperators error: \(String(describing: r))")
throw r
}
func getServerOperatorsSync() throws -> ServerOperatorConditions {
let r = chatSendCmdSync(.apiGetServerOperators)
if case let .serverOperatorConditions(conditions) = r { return conditions }
logger.error("getServerOperators error: \(String(describing: r))")
throw r
}
func setServerOperators(operators: [ServerOperator]) async throws -> ServerOperatorConditions {
let r = await chatSendCmd(.apiSetServerOperators(operators: operators))
if case let .serverOperatorConditions(conditions) = r { return conditions }
logger.error("setServerOperators error: \(String(describing: r))")
throw r
}
func getUserServers() async throws -> [UserOperatorServers] {
let userId = try currentUserId("getUserServers")
let r = await chatSendCmd(.apiGetUserServers(userId: userId))
if case let .userServers(_, userServers) = r { return userServers }
logger.error("getUserServers error: \(String(describing: r))")
throw r
}
func setUserServers(userServers: [UserOperatorServers]) async throws {
let userId = try currentUserId("setUserServers")
let r = await chatSendCmd(.apiSetUserServers(userId: userId, userServers: userServers))
if case .cmdOk = r { return }
logger.error("setUserServers error: \(String(describing: r))")
throw r
}
func validateServers(userServers: [UserOperatorServers]) async throws -> [UserServersError] {
let userId = try currentUserId("validateServers")
let r = await chatSendCmd(.apiValidateServers(userId: userId, userServers: userServers))
if case let .userServersValidation(_, serverErrors) = r { return serverErrors }
logger.error("validateServers error: \(String(describing: r))")
throw r
}
func getUsageConditions() async throws -> (UsageConditions, String?, UsageConditions?) {
let r = await chatSendCmd(.apiGetUsageConditions)
if case let .usageConditions(usageConditions, conditionsText, acceptedConditions) = r { return (usageConditions, conditionsText, acceptedConditions) }
logger.error("getUsageConditions error: \(String(describing: r))")
throw r
}
func setConditionsNotified(conditionsId: Int64) async throws {
let r = await chatSendCmd(.apiSetConditionsNotified(conditionsId: conditionsId))
if case .cmdOk = r { return }
logger.error("setConditionsNotified error: \(String(describing: r))")
throw r
}
func acceptConditions(conditionsId: Int64, operatorIds: [Int64]) async throws -> ServerOperatorConditions {
let r = await chatSendCmd(.apiAcceptConditions(conditionsId: conditionsId, operatorIds: operatorIds))
if case let .serverOperatorConditions(conditions) = r { return conditions }
logger.error("acceptConditions error: \(String(describing: r))")
throw r
}
func getChatItemTTL() throws -> ChatItemTTL {
let userId = try currentUserId("getChatItemTTL")
return try chatItemTTLResponse(chatSendCmdSync(.apiGetChatItemTTL(userId: userId)))
@@ -579,12 +646,18 @@ func apiContactInfo(_ contactId: Int64) async throws -> (ConnectionStats?, Profi
throw r
}
func apiGroupMemberInfo(_ groupId: Int64, _ groupMemberId: Int64) throws -> (GroupMember, ConnectionStats?) {
func apiGroupMemberInfoSync(_ groupId: Int64, _ groupMemberId: Int64) throws -> (GroupMember, ConnectionStats?) {
let r = chatSendCmdSync(.apiGroupMemberInfo(groupId: groupId, groupMemberId: groupMemberId))
if case let .groupMemberInfo(_, _, member, connStats_) = r { return (member, connStats_) }
throw r
}
func apiGroupMemberInfo(_ groupId: Int64, _ groupMemberId: Int64) async throws -> (GroupMember, ConnectionStats?) {
let r = await chatSendCmd(.apiGroupMemberInfo(groupId: groupId, groupMemberId: groupMemberId))
if case let .groupMemberInfo(_, _, member, connStats_) = r { return (member, connStats_) }
throw r
}
func apiContactQueueInfo(_ contactId: Int64) async throws -> (RcvMsgInfo?, ServerQueueInfo) {
let r = await chatSendCmd(.apiContactQueueInfo(contactId: contactId))
if case let .queueInfo(_, rcvMsgInfo, queueInfo) = r { return (rcvMsgInfo, queueInfo) }
@@ -639,8 +712,8 @@ func apiGetContactCode(_ contactId: Int64) async throws -> (Contact, String) {
throw r
}
func apiGetGroupMemberCode(_ groupId: Int64, _ groupMemberId: Int64) throws -> (GroupMember, String) {
let r = chatSendCmdSync(.apiGetGroupMemberCode(groupId: groupId, groupMemberId: groupMemberId))
func apiGetGroupMemberCode(_ groupId: Int64, _ groupMemberId: Int64) async throws -> (GroupMember, String) {
let r = await chatSendCmd(.apiGetGroupMemberCode(groupId: groupId, groupMemberId: groupMemberId))
if case let .groupMemberCode(_, _, member, connectionCode) = r { return (member, connectionCode) }
throw r
}
@@ -731,6 +804,12 @@ func apiConnect_(incognito: Bool, connReq: String) async -> ((ConnReqType, Pendi
message: "Unless your contact deleted the connection or this link was already used, it might be a bug - please report it.\nTo connect, please ask your contact to create another connection link and check that you have a stable network connection."
)
return (nil, alert)
case .chatCmdError(_, .errorAgent(.SMP(_, .QUOTA))):
let alert = mkAlert(
title: "Undelivered messages",
message: "The connection reached the limit of undelivered messages, your contact may be offline."
)
return (nil, alert)
case let .chatCmdError(_, .errorAgent(.INTERNAL(internalErr))):
if internalErr == "SEUniqueID" {
let alert = mkAlert(
@@ -996,8 +1075,12 @@ func apiRejectContactRequest(contactReqId: Int64) async throws {
throw r
}
func apiChatRead(type: ChatType, id: Int64, itemRange: (Int64, Int64)) async throws {
try await sendCommandOkResp(.apiChatRead(type: type, id: id, itemRange: itemRange))
func apiChatRead(type: ChatType, id: Int64) async throws {
try await sendCommandOkResp(.apiChatRead(type: type, id: id))
}
func apiChatItemsRead(type: ChatType, id: Int64, itemIds: [Int64]) async throws {
try await sendCommandOkResp(.apiChatItemsRead(type: type, id: id, itemIds: itemIds))
}
func apiChatUnread(type: ChatType, id: Int64, unreadChat: Bool) async throws {
@@ -1035,77 +1118,122 @@ func standaloneFileInfo(url: String, ctrl: chat_ctrl? = nil) async -> MigrationF
}
func receiveFile(user: any UserLike, fileId: Int64, userApprovedRelays: Bool = false, auto: Bool = false) async {
if let chatItem = await apiReceiveFile(
fileId: fileId,
userApprovedRelays: userApprovedRelays || !privacyAskToApproveRelaysGroupDefault.get(),
encrypted: privacyEncryptLocalFilesGroupDefault.get(),
await receiveFiles(
user: user,
fileIds: [fileId],
userApprovedRelays: userApprovedRelays,
auto: auto
) {
await chatItemSimpleUpdate(user, chatItem)
}
)
}
func apiReceiveFile(fileId: Int64, userApprovedRelays: Bool, encrypted: Bool, inline: Bool? = nil, auto: Bool = false) async -> AChatItem? {
let r = await chatSendCmd(.receiveFile(fileId: fileId, userApprovedRelays: userApprovedRelays, encrypted: encrypted, inline: inline))
let am = AlertManager.shared
if case let .rcvFileAccepted(_, chatItem) = r { return chatItem }
if case .rcvFileAcceptedSndCancelled = r {
logger.debug("apiReceiveFile error: sender cancelled file transfer")
if !auto {
am.showAlertMsg(
title: "Cannot receive file",
message: "Sender cancelled file transfer."
func receiveFiles(user: any UserLike, fileIds: [Int64], userApprovedRelays: Bool = false, auto: Bool = false) async {
var fileIdsToApprove = [Int64]()
var srvsToApprove = Set<String>()
var otherFileErrs = [ChatResponse]()
for fileId in fileIds {
let r = await chatSendCmd(
.receiveFile(
fileId: fileId,
userApprovedRelays: userApprovedRelays || !privacyAskToApproveRelaysGroupDefault.get(),
encrypted: privacyEncryptLocalFilesGroupDefault.get(),
inline: nil
)
)
switch r {
case let .rcvFileAccepted(_, chatItem):
await chatItemSimpleUpdate(user, chatItem)
default:
if let chatError = chatError(r) {
switch chatError {
case let .fileNotApproved(fileId, unknownServers):
fileIdsToApprove.append(fileId)
srvsToApprove.formUnion(unknownServers)
default:
otherFileErrs.append(r)
}
}
}
} else if let networkErrorAlert = networkErrorAlert(r) {
logger.error("apiReceiveFile network error: \(String(describing: r))")
if !auto {
am.showAlert(networkErrorAlert)
}
if !auto {
let otherErrsStr = if otherFileErrs.isEmpty {
""
} else if otherFileErrs.count == 1 {
"\(otherFileErrs[0])"
} else if otherFileErrs.count == 2 {
"\(otherFileErrs[0])\n\(otherFileErrs[1])"
} else {
"\(otherFileErrs[0])\n\(otherFileErrs[1])\nand \(otherFileErrs.count - 2) other error(s)"
}
} else {
switch chatError(r) {
case .fileCancelled:
logger.debug("apiReceiveFile ignoring fileCancelled error")
case .fileAlreadyReceiving:
logger.debug("apiReceiveFile ignoring fileAlreadyReceiving error")
case let .fileNotApproved(fileId, unknownServers):
logger.debug("apiReceiveFile fileNotApproved error")
if !auto {
let srvs = unknownServers.map { s in
// If there are not approved files, alert is shown the same way both in case of singular and plural files reception
if !fileIdsToApprove.isEmpty {
let srvs = srvsToApprove
.map { s in
if let srv = parseServerAddress(s), !srv.hostnames.isEmpty {
srv.hostnames[0]
} else {
serverHost(s)
}
}
am.showAlert(Alert(
title: Text("Unknown servers!"),
message: Text("Without Tor or VPN, your IP address will be visible to these XFTP relays: \(srvs.sorted().joined(separator: ", "))."),
primaryButton: .default(
Text("Download"),
action: {
Task {
logger.debug("apiReceiveFile fileNotApproved alert - in Task")
if let user = ChatModel.shared.currentUser {
await receiveFile(user: user, fileId: fileId, userApprovedRelays: true)
}
.sorted()
.joined(separator: ", ")
let fIds = fileIdsToApprove
await MainActor.run {
showAlert(
title: NSLocalizedString("Unknown servers!", comment: "alert title"),
message: (
String.localizedStringWithFormat(NSLocalizedString("Without Tor or VPN, your IP address will be visible to these XFTP relays: %@.", comment: "alert message"), srvs) +
(otherErrsStr != "" ? "\n\n" + String.localizedStringWithFormat(NSLocalizedString("Other file errors:\n%@", comment: "alert message"), otherErrsStr) : "")
),
buttonTitle: NSLocalizedString("Download", comment: "alert button"),
buttonAction: {
Task {
logger.debug("apiReceiveFile fileNotApproved alert - in Task")
if let user = ChatModel.shared.currentUser {
await receiveFiles(user: user, fileIds: fIds, userApprovedRelays: true)
}
}
),
secondaryButton: .cancel()
))
},
cancelButton: true
)
}
default:
logger.error("apiReceiveFile error: \(String(describing: r))")
if !auto {
am.showAlertMsg(
title: "Error receiving file",
message: "Error: \(responseError(r))"
} else if otherFileErrs.count == 1 { // If there is a single other error, we differentiate on it
let errorResponse = otherFileErrs.first!
switch errorResponse {
case let .rcvFileAcceptedSndCancelled(_, rcvFileTransfer):
logger.debug("receiveFiles error: sender cancelled file transfer \(rcvFileTransfer.fileId)")
await MainActor.run {
showAlert(
NSLocalizedString("Cannot receive file", comment: "alert title"),
message: NSLocalizedString("Sender cancelled file transfer.", comment: "alert message")
)
}
default:
if let chatError = chatError(errorResponse) {
switch chatError {
case .fileCancelled, .fileAlreadyReceiving:
logger.debug("receiveFiles ignoring FileCancelled or FileAlreadyReceiving error")
default:
await MainActor.run {
showAlert(
NSLocalizedString("Error receiving file", comment: "alert title"),
message: responseError(errorResponse)
)
}
}
}
}
} else if otherFileErrs.count > 1 { // If there are multiple other errors, we show general alert
await MainActor.run {
showAlert(
NSLocalizedString("Error receiving file", comment: "alert title"),
message: String.localizedStringWithFormat(NSLocalizedString("File errors:\n%@", comment: "alert message"), otherErrsStr)
)
}
}
}
return nil
}
func cancelFile(user: User, fileId: Int64) async {
@@ -1254,15 +1382,13 @@ func apiGetNetworkStatuses() throws -> [ConnNetworkStatus] {
throw r
}
func markChatRead(_ chat: Chat, aboveItem: ChatItem? = nil) async {
func markChatRead(_ chat: Chat) async {
do {
if chat.chatStats.unreadCount > 0 {
let minItemId = chat.chatStats.minUnreadItemId
let itemRange = (minItemId, aboveItem?.id ?? chat.chatItems.last?.id ?? minItemId)
let cInfo = chat.chatInfo
try await apiChatRead(type: cInfo.chatType, id: cInfo.apiId, itemRange: itemRange)
try await apiChatRead(type: cInfo.chatType, id: cInfo.apiId)
await MainActor.run {
withAnimation { ChatModel.shared.markChatItemsRead(cInfo, aboveItem: aboveItem) }
withAnimation { ChatModel.shared.markChatItemsRead(cInfo) }
}
}
if chat.chatStats.unreadChat {
@@ -1285,13 +1411,14 @@ func markChatUnread(_ chat: Chat, unreadChat: Bool = true) async {
}
}
func apiMarkChatItemRead(_ cInfo: ChatInfo, _ cItem: ChatItem) async {
func apiMarkChatItemsRead(_ cInfo: ChatInfo, _ itemIds: [ChatItem.ID]) async {
do {
logger.debug("apiMarkChatItemRead: \(cItem.id)")
try await apiChatRead(type: cInfo.chatType, id: cInfo.apiId, itemRange: (cItem.id, cItem.id))
await ChatModel.shared.markChatItemRead(cInfo, cItem)
try await apiChatItemsRead(type: cInfo.chatType, id: cInfo.apiId, itemIds: itemIds)
DispatchQueue.main.async {
ChatModel.shared.markChatItemsRead(cInfo, itemIds)
}
} catch {
logger.error("apiMarkChatItemRead apiChatRead error: \(responseError(error))")
logger.error("apiChatItemsRead error: \(responseError(error))")
}
}
@@ -1479,6 +1606,16 @@ func initializeChat(start: Bool, confirmStart: Bool = false, dbKey: String? = ni
try apiSetEncryptLocalFiles(privacyEncryptLocalFilesGroupDefault.get())
m.chatInitialized = true
m.currentUser = try apiGetActiveUser()
m.conditions = try getServerOperatorsSync()
if shouldImportAppSettingsDefault.get() {
do {
let appSettings = try apiGetAppSettings(settings: AppSettings.current.prepareForExport())
appSettings.importIntoApp()
shouldImportAppSettingsDefault.set(false)
} catch {
logger.error("Error while importing app settings: \(error)")
}
}
if m.currentUser == nil {
onboardingStageDefault.set(.step1_SimpleXInfo)
privacyDeliveryReceiptsSet.set(true)
@@ -1523,7 +1660,7 @@ private func chatInitialized(start: Bool, refreshInvitations: Bool) throws {
}
}
func startChat(refreshInvitations: Bool = true) throws {
func startChat(refreshInvitations: Bool = true, onboarding: Bool = false) throws {
logger.debug("startChat")
let m = ChatModel.shared
try setNetworkConfig(getNetCfg())
@@ -1542,13 +1679,15 @@ func startChat(refreshInvitations: Bool = true) throws {
if let token = m.deviceToken {
registerToken(token: token)
}
withAnimation {
let savedOnboardingStage = onboardingStageDefault.get()
m.onboardingStage = [.step1_SimpleXInfo, .step2_CreateProfile].contains(savedOnboardingStage) && m.users.count == 1
? .step3_CreateSimpleXAddress
: savedOnboardingStage
if m.onboardingStage == .onboardingComplete && !privacyDeliveryReceiptsSet.get() {
m.setDeliveryReceipts = true
if !onboarding {
withAnimation {
let savedOnboardingStage = onboardingStageDefault.get()
m.onboardingStage = [.step1_SimpleXInfo, .step2_CreateProfile].contains(savedOnboardingStage) && m.users.count == 1
? .step3_ChooseServerOperators
: savedOnboardingStage
if m.onboardingStage == .onboardingComplete && !privacyDeliveryReceiptsSet.get() {
m.setDeliveryReceipts = true
}
}
}
}
@@ -1805,23 +1944,25 @@ func processReceivedMsg(_ res: ChatResponse) async {
NtfManager.shared.notifyMessageReceived(user, cInfo, cItem)
}
}
case let .chatItemStatusUpdated(user, aChatItem):
let cInfo = aChatItem.chatInfo
let cItem = aChatItem.chatItem
if !cItem.isDeletedContent && active(user) {
await MainActor.run { m.updateChatItem(cInfo, cItem, status: cItem.meta.itemStatus) }
}
if let endTask = m.messageDelivery[cItem.id] {
switch cItem.meta.itemStatus {
case .sndNew: ()
case .sndSent: endTask()
case .sndRcvd: endTask()
case .sndErrorAuth: endTask()
case .sndError: endTask()
case .sndWarning: endTask()
case .rcvNew: ()
case .rcvRead: ()
case .invalid: ()
case let .chatItemsStatusesUpdated(user, chatItems):
for chatItem in chatItems {
let cInfo = chatItem.chatInfo
let cItem = chatItem.chatItem
if !cItem.isDeletedContent && active(user) {
await MainActor.run { m.updateChatItem(cInfo, cItem, status: cItem.meta.itemStatus) }
}
if let endTask = m.messageDelivery[cItem.id] {
switch cItem.meta.itemStatus {
case .sndNew: ()
case .sndSent: endTask()
case .sndRcvd: endTask()
case .sndErrorAuth: endTask()
case .sndError: endTask()
case .sndWarning: endTask()
case .rcvNew: ()
case .rcvRead: ()
case .invalid: ()
}
}
}
case let .chatItemUpdated(user, aChatItem):
@@ -1880,6 +2021,18 @@ func processReceivedMsg(_ res: ChatResponse) async {
m.removeChat(hostConn.id)
}
}
case let .businessLinkConnecting(user, groupInfo, _, fromContact):
if !active(user) { return }
await MainActor.run {
m.updateGroup(groupInfo)
}
if m.chatId == fromContact.id {
ItemsModel.shared.loadOpenChat(groupInfo.id)
}
await MainActor.run {
m.removeChat(fromContact.id)
}
case let .joinedGroupMemberConnecting(user, groupInfo, _, member):
if active(user) {
await MainActor.run {
@@ -2015,7 +2168,6 @@ func processReceivedMsg(_ res: ChatResponse) async {
await withCall(contact) { call in
await MainActor.run {
call.callState = .offerReceived
call.peerMedia = callType.media
call.sharedKey = sharedKey
}
let useRelay = UserDefaults.standard.bool(forKey: DEFAULT_WEBRTC_POLICY_RELAY)
+13 -6
View File
@@ -82,11 +82,17 @@ struct SimpleXApp: App {
if appState != .stopped {
startChatAndActivate {
if appState.inactive && chatModel.chatRunning == true {
Task {
await updateChats()
if !chatModel.showCallView && !CallController.shared.hasActiveCalls() {
await updateCallInvitations()
if chatModel.chatRunning == true {
if let ntfResponse = chatModel.notificationResponse {
chatModel.notificationResponse = nil
NtfManager.shared.processNotificationResponse(ntfResponse)
}
if appState.inactive {
Task {
await updateChats()
if !chatModel.showCallView && !CallController.shared.hasActiveCalls() {
await updateCallInvitations()
}
}
}
}
@@ -137,7 +143,8 @@ struct SimpleXApp: App {
let chats = try await apiGetChatsAsync()
await MainActor.run { chatModel.updateChats(chats) }
if let id = chatModel.chatId,
let chat = chatModel.getChat(id) {
let chat = chatModel.getChat(id),
!NtfManager.shared.navigatingToChat {
Task { await loadChat(chat: chat, clearItems: false) }
}
if let ncr = chatModel.ntfContactRequest {
+2 -2
View File
@@ -102,7 +102,7 @@ extension ThemeWallpaper {
public func importFromString() -> ThemeWallpaper {
if preset == nil, let image {
// Need to save image from string and to save its path
if let parsed = UIImage(base64Encoded: image),
if let parsed = imageFromBase64(image),
let filename = saveWallpaperFile(image: parsed) {
var copy = self
copy.image = nil
@@ -122,7 +122,7 @@ extension ThemeWallpaper {
let preset: String? = if case let WallpaperType.preset(filename, _) = type { filename } else { nil }
let scale: Float? = if case let WallpaperType.preset(_, scale) = type { scale } else { if case let WallpaperType.image(_, scale, _) = type { scale } else { 1.0 } }
let scaleType: WallpaperScaleType? = if case let WallpaperType.image(_, _, scaleType) = type { scaleType } else { nil }
let image: String? = if case WallpaperType.image = type, let image = type.uiImage { resizeImageToStrSize(image, maxDataSize: 5_000_000) } else { nil }
let image: String? = if case WallpaperType.image = type, let image = type.uiImage { resizeImageToStrSizeSync(image, maxDataSize: 5_000_000) } else { nil }
return ThemeWallpaper (
preset: preset,
scale: scale,
+1 -1
View File
@@ -197,7 +197,7 @@ class ThemeManager {
var themeIds = currentThemeIdsDefault.get()
themeIds[nonSystemThemeName] = prevValue.themeId
currentThemeIdsDefault.set(themeIds)
applyTheme(nonSystemThemeName)
applyTheme(currentThemeDefault.get())
}
static func copyFromSameThemeOverrides(_ type: WallpaperType?, _ lowerLevelOverride: ThemeModeOverride?, _ pref: Binding<ThemeModeOverride>) -> Bool {
+165 -113
View File
@@ -17,8 +17,8 @@ struct ActiveCallView: View {
@ObservedObject var call: Call
@Environment(\.scenePhase) var scenePhase
@State private var client: WebRTCClient? = nil
@State private var activeCall: WebRTCClient.Call? = nil
@State private var localRendererAspectRatio: CGFloat? = nil
@State var remoteContentMode: UIView.ContentMode = .scaleAspectFill
@Binding var canConnectCall: Bool
@State var prevColorScheme: ColorScheme = .dark
@State var pipShown = false
@@ -27,24 +27,39 @@ struct ActiveCallView: View {
var body: some View {
ZStack(alignment: .topLeading) {
ZStack(alignment: .bottom) {
if let client = client, [call.peerMedia, call.localMedia].contains(.video), activeCall != nil {
if let client = client, call.hasVideo {
GeometryReader { g in
let width = g.size.width * 0.3
ZStack(alignment: .topTrailing) {
CallViewRemote(client: client, activeCall: $activeCall, activeCallViewIsCollapsed: $m.activeCallViewIsCollapsed, pipShown: $pipShown)
CallViewLocal(client: client, activeCall: $activeCall, localRendererAspectRatio: $localRendererAspectRatio, pipShown: $pipShown)
.cornerRadius(10)
.frame(width: width, height: width / (localRendererAspectRatio ?? 1))
.padding([.top, .trailing], 17)
ZStack(alignment: .center) {
// For some reason, when the view in GeometryReader and ZStack is visible, it steals clicks on a back button, so showing something on top like this with background color helps (.clear color doesn't work)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.primary.opacity(0.000001))
CallViewRemote(client: client, call: call, activeCallViewIsCollapsed: $m.activeCallViewIsCollapsed, contentMode: $remoteContentMode, pipShown: $pipShown)
.onTapGesture {
remoteContentMode = remoteContentMode == .scaleAspectFill ? .scaleAspectFit : .scaleAspectFill
}
Group {
let localVideoTrack = client.activeCall?.localVideoTrack ?? client.notConnectedCall?.localCameraAndTrack?.1
if localVideoTrack != nil {
CallViewLocal(client: client, localRendererAspectRatio: $localRendererAspectRatio, pipShown: $pipShown)
.onDisappear {
localRendererAspectRatio = nil
}
} else {
Rectangle().fill(.black)
}
}
.cornerRadius(10)
.frame(width: width, height: localRendererAspectRatio == nil ? (g.size.width < g.size.height ? width * 1.33 : width / 1.33) : width / (localRendererAspectRatio ?? 1))
.padding([.top, .trailing], 17)
}
}
}
if let call = m.activeCall, let client = client, (!pipShown || !call.supportsVideo) {
if let call = m.activeCall, let client = client, (!pipShown || !call.hasVideo) {
ActiveCallOverlay(call: call, client: client)
}
}
@@ -54,6 +69,9 @@ struct ActiveCallView: View {
.onAppear {
logger.debug("ActiveCallView: appear client is nil \(client == nil), scenePhase \(String(describing: scenePhase)), canConnectCall \(canConnectCall)")
AppDelegate.keepScreenOn(true)
Task {
await askRequiredPermissions()
}
createWebRTCClient()
dismissAllSheets()
hideKeyboard()
@@ -84,7 +102,7 @@ struct ActiveCallView: View {
private func createWebRTCClient() {
if client == nil && canConnectCall {
client = WebRTCClient($activeCall, { msg in await MainActor.run { processRtcMessage(msg: msg) } }, $localRendererAspectRatio)
client = WebRTCClient({ msg in await MainActor.run { processRtcMessage(msg: msg) } }, $localRendererAspectRatio)
Task {
await m.callCommand.setClient(client)
}
@@ -99,7 +117,7 @@ struct ActiveCallView: View {
logger.debug("ActiveCallView: response \(msg.resp.respType)")
switch msg.resp {
case let .capabilities(capabilities):
let callType = CallType(media: call.localMedia, capabilities: capabilities)
let callType = CallType(media: call.initialCallType, capabilities: capabilities)
Task {
do {
try await apiSendCallInvitation(call.contact, callType)
@@ -110,7 +128,7 @@ struct ActiveCallView: View {
call.callState = .invitationSent
call.localCapabilities = capabilities
}
if call.supportsVideo && !AVAudioSession.sharedInstance().hasExternalAudioDevice() {
if call.hasVideo && !AVAudioSession.sharedInstance().hasExternalAudioDevice() {
try? AVAudioSession.sharedInstance().setCategory(.playback, options: [.allowBluetooth, .allowAirPlay, .allowBluetoothA2DP])
}
CallSoundsPlayer.shared.startConnectingCallSound()
@@ -120,7 +138,7 @@ struct ActiveCallView: View {
Task {
do {
try await apiSendCallOffer(call.contact, offer, iceCandidates,
media: call.localMedia, capabilities: capabilities)
media: call.initialCallType, capabilities: capabilities)
} catch {
logger.error("apiSendCallOffer \(responseError(error))")
}
@@ -164,6 +182,9 @@ struct ActiveCallView: View {
}
if state.connectionState == "closed" {
closeCallView(client)
if let callUUID = m.activeCall?.callUUID {
CallController.shared.endCall(callUUID: callUUID)
}
m.activeCall = nil
m.activeCallViewIsCollapsed = false
}
@@ -182,6 +203,14 @@ struct ActiveCallView: View {
CallSoundsPlayer.shared.vibrate(long: false)
wasConnected = true
}
case let .peerMedia(source, enabled):
switch source {
case .mic: call.peerMediaSources.mic = enabled
case .camera: call.peerMediaSources.camera = enabled
case .screenAudio: call.peerMediaSources.screenAudio = enabled
case .screenVideo: call.peerMediaSources.screenVideo = enabled
case .unknown: ()
}
case .ended:
closeCallView(client)
call.callState = .ended
@@ -214,16 +243,38 @@ struct ActiveCallView: View {
ChatReceiver.shared.messagesChannel = nil
return
}
if case let .chatItemStatusUpdated(_, msg) = msg,
msg.chatInfo.id == call.contact.id,
case .sndCall = msg.chatItem.content,
case .sndRcvd = msg.chatItem.meta.itemStatus {
if case let .chatItemsStatusesUpdated(_, chatItems) = msg,
chatItems.contains(where: { ci in
ci.chatInfo.id == call.contact.id &&
ci.chatItem.content.isSndCall &&
ci.chatItem.meta.itemStatus.isSndRcvd
}) {
CallSoundsPlayer.shared.startInCallSound()
ChatReceiver.shared.messagesChannel = nil
}
}
}
private func askRequiredPermissions() async {
let mic = await WebRTCClient.isAuthorized(for: .audio)
await MainActor.run {
call.localMediaSources.mic = mic
}
let cameraAuthorized = AVCaptureDevice.authorizationStatus(for: .video) == .authorized
var camera = call.initialCallType == .audio || cameraAuthorized
if call.initialCallType == .video && !cameraAuthorized {
camera = await WebRTCClient.isAuthorized(for: .video)
await MainActor.run {
if camera, let client {
client.setCameraEnabled(true)
}
}
}
if !mic || !camera {
WebRTCClient.showUnauthorizedAlert(for: !mic ? .audio : .video)
}
}
private func closeCallView(_ client: WebRTCClient) {
if m.activeCall != nil {
m.showCallView = false
@@ -239,44 +290,16 @@ struct ActiveCallOverlay: View {
var body: some View {
VStack {
switch call.localMedia {
case .video:
switch call.hasVideo {
case true:
videoCallInfoView(call)
.foregroundColor(.white)
.opacity(0.8)
.padding()
Spacer()
HStack {
toggleAudioButton()
Spacer()
if deviceManager.availableInputs.allSatisfy({ $0.portType == .builtInMic }) {
toggleSpeakerButton()
.frame(width: 40, height: 40)
} else if call.hasMedia {
AudioDevicePicker()
.scaleEffect(2)
.frame(maxWidth: 40, maxHeight: 40)
} else {
Color.clear.frame(width: 40, height: 40)
}
Spacer()
endCallButton()
Spacer()
if call.videoEnabled {
flipCameraButton()
} else {
Color.clear.frame(width: 40, height: 40)
}
Spacer()
toggleVideoButton()
}
.padding(.horizontal, 20)
.padding(.bottom, 16)
.frame(maxWidth: .infinity, alignment: .center)
case .audio:
.padding(.horizontal)
// Fixed vertical padding required for preserving position of buttons row when changing audio-to-video and back in landscape orientation.
// Otherwise, bigger padding is added by SwiftUI when switching call types
.padding(.vertical, 10)
case false:
ZStack(alignment: .topLeading) {
Button {
chatModel.activeCallViewIsCollapsed = true
@@ -291,35 +314,32 @@ struct ActiveCallOverlay: View {
}
.foregroundColor(.white)
.opacity(0.8)
.padding()
.padding(.horizontal)
.padding(.vertical, 10)
.frame(maxHeight: .infinity)
}
Spacer()
ZStack(alignment: .bottom) {
toggleAudioButton()
.frame(maxWidth: .infinity, alignment: .leading)
endCallButton()
// Check if the only input is microphone. And in this case show toggle button,
// If there are more inputs, it probably means something like bluetooth headphones are available
// and in this case show iOS button for choosing different output.
// There is no way to get available outputs, only inputs
if deviceManager.availableInputs.allSatisfy({ $0.portType == .builtInMic }) {
toggleSpeakerButton()
.frame(maxWidth: .infinity, alignment: .trailing)
} else if call.hasMedia {
AudioDevicePicker()
.scaleEffect(2)
.frame(maxWidth: 50, maxHeight: 40)
.frame(maxWidth: .infinity, alignment: .trailing)
} else {
Color.clear.frame(width: 50, height: 40)
}
}
.padding(.bottom, 60)
.padding(.horizontal, 48)
}
Spacer()
HStack {
toggleMicButton()
Spacer()
audioDeviceButton()
Spacer()
endCallButton()
Spacer()
if call.localMediaSources.camera {
flipCameraButton()
} else {
Color.clear.frame(width: 60, height: 60)
}
Spacer()
toggleCameraButton()
}
.padding(.horizontal, 20)
.padding(.bottom, 16)
.frame(maxWidth: 440, alignment: .center)
}
.frame(maxWidth: .infinity)
.onAppear {
@@ -381,36 +401,54 @@ struct ActiveCallOverlay: View {
private func endCallButton() -> some View {
let cc = CallController.shared
return callButton("phone.down.fill", width: 60, height: 60) {
return callButton("phone.down.fill", .red, padding: 10) {
if let uuid = call.callUUID {
cc.endCall(callUUID: uuid)
} else {
cc.endCall(call: call) {}
}
}
.foregroundColor(.red)
}
private func toggleAudioButton() -> some View {
controlButton(call, call.audioEnabled ? "mic.fill" : "mic.slash") {
private func toggleMicButton() -> some View {
controlButton(call, call.localMediaSources.mic ? "mic.fill" : "mic.slash", padding: 14) {
Task {
client.setAudioEnabled(!call.audioEnabled)
DispatchQueue.main.async {
call.audioEnabled = !call.audioEnabled
}
if await WebRTCClient.isAuthorized(for: .audio) {
client.setAudioEnabled(!call.localMediaSources.mic)
} else { WebRTCClient.showUnauthorizedAlert(for: .audio) }
}
}
}
func audioDeviceButton() -> some View {
// Check if the only input is microphone. And in this case show toggle button,
// If there are more inputs, it probably means something like bluetooth headphones are available
// and in this case show iOS button for choosing different output.
// There is no way to get available outputs, only inputs
Group {
if deviceManager.availableInputs.allSatisfy({ $0.portType == .builtInMic }) {
toggleSpeakerButton()
} else {
audioDevicePickerButton()
}
}
.onChange(of: call.localMediaSources.hasVideo) { hasVideo in
let current = AVAudioSession.sharedInstance().currentRoute.outputs.first?.portType
let speakerEnabled = current == .builtInSpeaker
let receiverEnabled = current == .builtInReceiver
// react automatically only when receiver were selected, otherwise keep an external device selected
if !speakerEnabled && hasVideo && receiverEnabled {
client.setSpeakerEnabledAndConfigureSession(!speakerEnabled, skipExternalDevice: true)
call.speakerEnabled = !speakerEnabled
}
}
}
private func toggleSpeakerButton() -> some View {
controlButton(call, call.speakerEnabled ? "speaker.wave.2.fill" : "speaker.wave.1.fill") {
Task {
let speakerEnabled = AVAudioSession.sharedInstance().currentRoute.outputs.first?.portType == .builtInSpeaker
client.setSpeakerEnabledAndConfigureSession(!speakerEnabled)
DispatchQueue.main.async {
call.speakerEnabled = !speakerEnabled
}
}
controlButton(call, !call.peerMediaSources.mic ? "speaker.slash" : call.speakerEnabled ? "speaker.wave.2.fill" : "speaker.wave.1.fill", padding: !call.peerMediaSources.mic ? 16 : call.speakerEnabled ? 15 : 17) {
let speakerEnabled = AVAudioSession.sharedInstance().currentRoute.outputs.first?.portType == .builtInSpeaker
client.setSpeakerEnabledAndConfigureSession(!speakerEnabled)
call.speakerEnabled = !speakerEnabled
}
.onAppear {
deviceManager.call = call
@@ -418,53 +456,67 @@ struct ActiveCallOverlay: View {
}
}
private func toggleVideoButton() -> some View {
controlButton(call, call.videoEnabled ? "video.fill" : "video.slash") {
private func toggleCameraButton() -> some View {
controlButton(call, call.localMediaSources.camera ? "video.fill" : "video.slash", padding: call.localMediaSources.camera ? 16 : 14) {
Task {
client.setVideoEnabled(!call.videoEnabled)
DispatchQueue.main.async {
call.videoEnabled = !call.videoEnabled
}
if await WebRTCClient.isAuthorized(for: .video) {
client.setCameraEnabled(!call.localMediaSources.camera)
} else { WebRTCClient.showUnauthorizedAlert(for: .video) }
}
}
.disabled(call.initialCallType == .audio && client.activeCall?.peerHasOldVersion == true)
}
@ViewBuilder private func flipCameraButton() -> some View {
controlButton(call, "arrow.triangle.2.circlepath") {
controlButton(call, "arrow.triangle.2.circlepath", padding: 12) {
Task {
client.flipCamera()
if await WebRTCClient.isAuthorized(for: .video) {
client.flipCamera()
}
}
}
}
@ViewBuilder private func controlButton(_ call: Call, _ imageName: String, _ perform: @escaping () -> Void) -> some View {
if call.hasMedia {
callButton(imageName, width: 50, height: 38, perform)
.foregroundColor(.white)
.opacity(0.85)
} else {
Color.clear.frame(width: 50, height: 38)
}
@ViewBuilder private func controlButton(_ call: Call, _ imageName: String, padding: CGFloat, _ perform: @escaping () -> Void) -> some View {
callButton(imageName, call.peerMediaSources.hasVideo ? Color.black.opacity(0.2) : Color.white.opacity(0.2), padding: padding, perform)
}
private func callButton(_ imageName: String, width: CGFloat, height: CGFloat, _ perform: @escaping () -> Void) -> some View {
@ViewBuilder private func audioDevicePickerButton() -> some View {
AudioDevicePicker()
.opacity(0.8)
.scaleEffect(2)
.padding(10)
.frame(width: 60, height: 60)
.background(call.peerMediaSources.hasVideo ? Color.black.opacity(0.2) : Color.white.opacity(0.2))
.clipShape(.circle)
}
private func callButton(_ imageName: String, _ background: Color, padding: CGFloat, _ perform: @escaping () -> Void) -> some View {
Button {
perform()
} label: {
Image(systemName: imageName)
.resizable()
.scaledToFit()
.frame(maxWidth: width, maxHeight: height)
.padding(padding)
.frame(width: 60, height: 60)
.background(background)
}
.foregroundColor(whiteColorWithAlpha)
.clipShape(.circle)
}
private var whiteColorWithAlpha: Color {
get { Color(red: 204 / 255, green: 204 / 255, blue: 204 / 255) }
}
}
struct ActiveCallOverlay_Previews: PreviewProvider {
static var previews: some View {
Group{
ActiveCallOverlay(call: Call(direction: .incoming, contact: Contact.sampleData, callUUID: UUID().uuidString.lowercased(), callState: .offerSent, localMedia: .video), client: WebRTCClient(Binding.constant(nil), { _ in }, Binding.constant(nil)))
ActiveCallOverlay(call: Call(direction: .incoming, contact: Contact.sampleData, callUUID: UUID().uuidString.lowercased(), callState: .offerSent, initialCallType: .video), client: WebRTCClient({ _ in }, Binding.constant(nil)))
.background(.black)
ActiveCallOverlay(call: Call(direction: .incoming, contact: Contact.sampleData, callUUID: UUID().uuidString.lowercased(), callState: .offerSent, localMedia: .audio), client: WebRTCClient(Binding.constant(nil), { _ in }, Binding.constant(nil)))
ActiveCallOverlay(call: Call(direction: .incoming, contact: Contact.sampleData, callUUID: UUID().uuidString.lowercased(), callState: .offerSent, initialCallType: .audio), client: WebRTCClient({ _ in }, Binding.constant(nil)))
.background(.black)
}
}
@@ -104,7 +104,7 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse
}
func provider(_ provider: CXProvider, perform action: CXSetMutedCallAction) {
if callManager.enableMedia(media: .audio, enable: !action.isMuted, callUUID: action.callUUID.uuidString.lowercased()) {
if callManager.enableMedia(source: .mic, enable: !action.isMuted, callUUID: action.callUUID.uuidString.lowercased()) {
action.fulfill()
} else {
action.fail()
@@ -121,8 +121,8 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse
RTCAudioSession.sharedInstance().audioSessionDidActivate(audioSession)
RTCAudioSession.sharedInstance().isAudioEnabled = true
do {
let supportsVideo = ChatModel.shared.activeCall?.supportsVideo == true
if supportsVideo {
let hasVideo = ChatModel.shared.activeCall?.hasVideo == true
if hasVideo {
try audioSession.setCategory(.playAndRecord, mode: .videoChat, options: [.defaultToSpeaker, .mixWithOthers, .allowBluetooth, .allowAirPlay, .allowBluetoothA2DP])
} else {
try audioSession.setCategory(.playAndRecord, mode: .voiceChat, options: [.mixWithOthers, .allowBluetooth, .allowAirPlay, .allowBluetoothA2DP])
@@ -133,7 +133,7 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse
try? await Task.sleep(nanoseconds: UInt64(i) * 300_000000)
if let preferred = audioSession.preferredInputDevice() {
await MainActor.run { try? audioSession.setPreferredInput(preferred) }
} else if supportsVideo {
} else if hasVideo {
await MainActor.run { try? audioSession.overrideOutputAudioPort(.speaker) }
}
}
@@ -357,11 +357,9 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse
self.provider.reportCall(with: uuid, updated: update)
}
} else if callManager.startOutgoingCall(callUUID: callUUID) {
if callManager.startOutgoingCall(callUUID: callUUID) {
logger.debug("CallController.startCall: call started")
} else {
logger.error("CallController.startCall: no active call")
}
logger.debug("CallController.startCall: call started")
} else {
logger.error("CallController.startCall: no active call")
}
}
+5 -5
View File
@@ -12,7 +12,7 @@ import SimpleXChat
class CallManager {
func newOutgoingCall(_ contact: Contact, _ media: CallMediaType) -> String {
let uuid = UUID().uuidString.lowercased()
let call = Call(direction: .outgoing, contact: contact, callUUID: uuid, callState: .waitCapabilities, localMedia: media)
let call = Call(direction: .outgoing, contact: contact, callUUID: uuid, callState: .waitCapabilities, initialCallType: media)
call.speakerEnabled = media == .video
ChatModel.shared.activeCall = call
return uuid
@@ -22,7 +22,7 @@ class CallManager {
let m = ChatModel.shared
if let call = m.activeCall, call.callUUID == callUUID {
m.showCallView = true
Task { await m.callCommand.processCommand(.capabilities(media: call.localMedia)) }
Task { await m.callCommand.processCommand(.capabilities(media: call.initialCallType)) }
return true
}
return false
@@ -44,7 +44,7 @@ class CallManager {
contact: invitation.contact,
callUUID: invitation.callUUID,
callState: .invitationAccepted,
localMedia: invitation.callType.media,
initialCallType: invitation.callType.media,
sharedKey: invitation.sharedKey
)
call.speakerEnabled = invitation.callType.media == .video
@@ -68,10 +68,10 @@ class CallManager {
}
}
func enableMedia(media: CallMediaType, enable: Bool, callUUID: String) -> Bool {
func enableMedia(source: CallMediaSource, enable: Bool, callUUID: String) -> Bool {
if let call = ChatModel.shared.activeCall, call.callUUID == callUUID {
let m = ChatModel.shared
Task { await m.callCommand.processCommand(.media(media: media, enable: enable)) }
Task { await m.callCommand.processCommand(.media(source: source, enable: enable)) }
return true
}
return false
@@ -10,40 +10,49 @@ import AVKit
struct CallViewRemote: UIViewRepresentable {
var client: WebRTCClient
var activeCall: Binding<WebRTCClient.Call?>
@ObservedObject var call: Call
@State var enablePip: (Bool) -> Void = {_ in }
@Binding var activeCallViewIsCollapsed: Bool
@Binding var contentMode: UIView.ContentMode
@Binding var pipShown: Bool
init(client: WebRTCClient, activeCall: Binding<WebRTCClient.Call?>, activeCallViewIsCollapsed: Binding<Bool>, pipShown: Binding<Bool>) {
self.client = client
self.activeCall = activeCall
self._activeCallViewIsCollapsed = activeCallViewIsCollapsed
self._pipShown = pipShown
}
func makeUIView(context: Context) -> UIView {
let view = UIView()
if let call = activeCall.wrappedValue {
let remoteRenderer = RTCMTLVideoView(frame: view.frame)
remoteRenderer.videoContentMode = .scaleAspectFill
client.addRemoteRenderer(call, remoteRenderer)
addSubviewAndResize(remoteRenderer, into: view)
let remoteCameraRenderer = RTCMTLVideoView(frame: view.frame)
remoteCameraRenderer.videoContentMode = contentMode
remoteCameraRenderer.tag = 0
if AVPictureInPictureController.isPictureInPictureSupported() {
makeViewWithRTCRenderer(call, remoteRenderer, view, context)
}
let screenVideo = call.peerMediaSources.screenVideo
let remoteScreenRenderer = RTCMTLVideoView(frame: view.frame)
remoteScreenRenderer.videoContentMode = contentMode
remoteScreenRenderer.tag = 1
remoteScreenRenderer.alpha = screenVideo ? 1 : 0
context.coordinator.cameraRenderer = remoteCameraRenderer
context.coordinator.screenRenderer = remoteScreenRenderer
client.addRemoteCameraRenderer(remoteCameraRenderer)
client.addRemoteScreenRenderer(remoteScreenRenderer)
if screenVideo {
addSubviewAndResize(remoteScreenRenderer, remoteCameraRenderer, into: view)
} else {
addSubviewAndResize(remoteCameraRenderer, remoteScreenRenderer, into: view)
}
if AVPictureInPictureController.isPictureInPictureSupported() {
makeViewWithRTCRenderer(remoteCameraRenderer, remoteScreenRenderer, view, context)
}
return view
}
func makeViewWithRTCRenderer(_ call: WebRTCClient.Call, _ remoteRenderer: RTCMTLVideoView, _ view: UIView, _ context: Context) {
let pipRemoteRenderer = RTCMTLVideoView(frame: view.frame)
pipRemoteRenderer.videoContentMode = .scaleAspectFill
func makeViewWithRTCRenderer(_ remoteCameraRenderer: RTCMTLVideoView, _ remoteScreenRenderer: RTCMTLVideoView, _ view: UIView, _ context: Context) {
let pipRemoteCameraRenderer = RTCMTLVideoView(frame: view.frame)
pipRemoteCameraRenderer.videoContentMode = .scaleAspectFill
let pipRemoteScreenRenderer = RTCMTLVideoView(frame: view.frame)
pipRemoteScreenRenderer.videoContentMode = .scaleAspectFill
let pipVideoCallViewController = AVPictureInPictureVideoCallViewController()
pipVideoCallViewController.preferredContentSize = CGSize(width: 1080, height: 1920)
addSubviewAndResize(pipRemoteRenderer, into: pipVideoCallViewController.view)
let pipContentSource = AVPictureInPictureController.ContentSource(
activeVideoCallSourceView: view,
contentViewController: pipVideoCallViewController
@@ -55,7 +64,9 @@ struct CallViewRemote: UIViewRepresentable {
context.coordinator.pipController = pipController
context.coordinator.willShowHide = { show in
if show {
client.addRemoteRenderer(call, pipRemoteRenderer)
client.addRemoteCameraRenderer(pipRemoteCameraRenderer)
client.addRemoteScreenRenderer(pipRemoteScreenRenderer)
context.coordinator.relayout()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
activeCallViewIsCollapsed = true
}
@@ -67,13 +78,29 @@ struct CallViewRemote: UIViewRepresentable {
}
context.coordinator.didShowHide = { show in
if show {
remoteRenderer.isHidden = true
remoteCameraRenderer.isHidden = true
remoteScreenRenderer.isHidden = true
} else {
client.removeRemoteRenderer(call, pipRemoteRenderer)
remoteRenderer.isHidden = false
client.removeRemoteCameraRenderer(pipRemoteCameraRenderer)
client.removeRemoteScreenRenderer(pipRemoteScreenRenderer)
remoteCameraRenderer.isHidden = false
remoteScreenRenderer.isHidden = false
}
pipShown = show
}
context.coordinator.relayout = {
let camera = call.peerMediaSources.camera
let screenVideo = call.peerMediaSources.screenVideo
pipRemoteCameraRenderer.alpha = camera ? 1 : 0
pipRemoteScreenRenderer.alpha = screenVideo ? 1 : 0
if screenVideo {
addSubviewAndResize(pipRemoteScreenRenderer, pipRemoteCameraRenderer, pip: true, into: pipVideoCallViewController.view)
} else {
addSubviewAndResize(pipRemoteCameraRenderer, pipRemoteScreenRenderer, pip: true, into: pipVideoCallViewController.view)
}
(pipVideoCallViewController.view.subviews[0] as! RTCMTLVideoView).videoContentMode = contentMode
(pipVideoCallViewController.view.subviews[1] as! RTCMTLVideoView).videoContentMode = .scaleAspectFill
}
DispatchQueue.main.async {
enablePip = { enable in
if enable != pipShown /* pipController.isPictureInPictureActive */ {
@@ -88,24 +115,50 @@ struct CallViewRemote: UIViewRepresentable {
}
func makeCoordinator() -> Coordinator {
Coordinator()
Coordinator(client)
}
func updateUIView(_ view: UIView, context: Context) {
logger.debug("CallView.updateUIView remote")
let camera = view.subviews.first(where: { $0.tag == 0 })!
let screen = view.subviews.first(where: { $0.tag == 1 })!
let screenVideo = call.peerMediaSources.screenVideo
if screenVideo && screen.alpha == 0 {
screen.alpha = 1
addSubviewAndResize(screen, camera, into: view)
} else if !screenVideo && screen.alpha == 1 {
screen.alpha = 0
addSubviewAndResize(camera, screen, into: view)
}
(view.subviews[0] as! RTCMTLVideoView).videoContentMode = contentMode
(view.subviews[1] as! RTCMTLVideoView).videoContentMode = .scaleAspectFill
camera.alpha = call.peerMediaSources.camera ? 1 : 0
screen.alpha = call.peerMediaSources.screenVideo ? 1 : 0
DispatchQueue.main.async {
if activeCallViewIsCollapsed != pipShown {
enablePip(activeCallViewIsCollapsed)
} else if pipShown {
context.coordinator.relayout()
}
}
}
// MARK: - Coordinator
class Coordinator: NSObject, AVPictureInPictureControllerDelegate {
var cameraRenderer: RTCMTLVideoView?
var screenRenderer: RTCMTLVideoView?
var client: WebRTCClient
var pipController: AVPictureInPictureController? = nil
var willShowHide: (Bool) -> Void = { _ in }
var didShowHide: (Bool) -> Void = { _ in }
var relayout: () -> Void = {}
required init(_ client: WebRTCClient) {
self.client = client
}
func pictureInPictureControllerWillStartPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) {
willShowHide(true)
}
@@ -127,11 +180,20 @@ struct CallViewRemote: UIViewRepresentable {
}
deinit {
// TODO: deinit is not called when changing call type from audio to video and back,
// which causes many renderers can be created and added to stream (if enabling/disabling
// video while not yet connected in outgoing call)
pipController?.stopPictureInPicture()
pipController?.canStartPictureInPictureAutomaticallyFromInline = false
pipController?.contentSource = nil
pipController?.delegate = nil
pipController = nil
if let cameraRenderer {
client.removeRemoteCameraRenderer(cameraRenderer)
}
if let screenRenderer {
client.removeRemoteScreenRenderer(screenRenderer)
}
}
}
@@ -148,51 +210,109 @@ struct CallViewRemote: UIViewRepresentable {
struct CallViewLocal: UIViewRepresentable {
var client: WebRTCClient
var activeCall: Binding<WebRTCClient.Call?>
var localRendererAspectRatio: Binding<CGFloat?>
@State var pipStateChanged: (Bool) -> Void = {_ in }
@Binding var pipShown: Bool
init(client: WebRTCClient, activeCall: Binding<WebRTCClient.Call?>, localRendererAspectRatio: Binding<CGFloat?>, pipShown: Binding<Bool>) {
init(client: WebRTCClient, localRendererAspectRatio: Binding<CGFloat?>, pipShown: Binding<Bool>) {
self.client = client
self.activeCall = activeCall
self.localRendererAspectRatio = localRendererAspectRatio
self._pipShown = pipShown
}
func makeUIView(context: Context) -> UIView {
let view = UIView()
if let call = activeCall.wrappedValue {
let localRenderer = RTCEAGLVideoView(frame: .zero)
client.addLocalRenderer(call, localRenderer)
client.startCaptureLocalVideo(call)
addSubviewAndResize(localRenderer, into: view)
DispatchQueue.main.async {
pipStateChanged = { shown in
localRenderer.isHidden = shown
}
let localRenderer = RTCEAGLVideoView(frame: .zero)
context.coordinator.renderer = localRenderer
client.addLocalRenderer(localRenderer)
addSubviewAndResize(localRenderer, nil, into: view)
DispatchQueue.main.async {
pipStateChanged = { shown in
localRenderer.isHidden = shown
}
}
return view
}
func makeCoordinator() -> Coordinator {
Coordinator(client)
}
func updateUIView(_ view: UIView, context: Context) {
logger.debug("CallView.updateUIView local")
pipStateChanged(pipShown)
}
// MARK: - Coordinator
class Coordinator: NSObject, AVPictureInPictureControllerDelegate {
var renderer: RTCEAGLVideoView?
var client: WebRTCClient
required init(_ client: WebRTCClient) {
self.client = client
}
deinit {
if let renderer {
client.removeLocalRenderer(renderer)
}
}
}
}
private func addSubviewAndResize(_ view: UIView, into containerView: UIView) {
containerView.addSubview(view)
view.translatesAutoresizingMaskIntoConstraints = false
containerView.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|[view]|",
options: [],
metrics: nil,
views: ["view": view]))
private func addSubviewAndResize(_ fullscreen: UIView, _ end: UIView?, pip: Bool = false, into containerView: UIView) {
if containerView.subviews.firstIndex(of: fullscreen) == 0 && ((end == nil && containerView.subviews.count == 1) || (end != nil && containerView.subviews.firstIndex(of: end!) == 1)) {
// Nothing to do, elements on their places
return
}
containerView.removeConstraints(containerView.constraints)
containerView.subviews.forEach { sub in sub.removeFromSuperview()}
containerView.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|[view]|",
containerView.addSubview(fullscreen)
fullscreen.translatesAutoresizingMaskIntoConstraints = false
fullscreen.layer.cornerRadius = 0
fullscreen.layer.masksToBounds = false
if let end {
containerView.addSubview(end)
end.translatesAutoresizingMaskIntoConstraints = false
end.layer.cornerRadius = pip ? 8 : 10
end.layer.masksToBounds = true
}
let constraintFullscreenV = NSLayoutConstraint.constraints(
withVisualFormat: "V:|[fullscreen]|",
options: [],
metrics: nil,
views: ["view": view]))
views: ["fullscreen": fullscreen]
)
let constraintFullscreenH = NSLayoutConstraint.constraints(
withVisualFormat: "H:|[fullscreen]|",
options: [],
metrics: nil,
views: ["fullscreen": fullscreen]
)
containerView.addConstraints(constraintFullscreenV)
containerView.addConstraints(constraintFullscreenH)
if let end {
let constraintEndWidth = NSLayoutConstraint(
item: end, attribute: .width, relatedBy: .equal, toItem: containerView, attribute: .width, multiplier: pip ? 0.5 : 0.3, constant: 0
)
let constraintEndHeight = NSLayoutConstraint(
item: end, attribute: .height, relatedBy: .equal, toItem: containerView, attribute: .width, multiplier: pip ? 0.5 * 1.33 : 0.3 * 1.33, constant: 0
)
let constraintEndX = NSLayoutConstraint(
item: end, attribute: .leading, relatedBy: .equal, toItem: containerView, attribute: .trailing, multiplier: pip ? 0.5 : 0.7, constant: pip ? -8 : -17
)
let constraintEndY = NSLayoutConstraint(
item: end, attribute: .bottom, relatedBy: .equal, toItem: containerView, attribute: .bottom, multiplier: 1, constant: pip ? -8 : -92
)
containerView.addConstraint(constraintEndWidth)
containerView.addConstraint(constraintEndHeight)
containerView.addConstraint(constraintEndX)
containerView.addConstraint(constraintEndY)
}
containerView.layoutIfNeeded()
}
@@ -38,6 +38,7 @@ struct IncomingCallView: View {
}
HStack {
ProfilePreview(profileOf: invitation.contact, color: .white)
.padding(.vertical, 6)
Spacer()
callButton("Reject", "phone.down.fill", .red) {
+39 -17
View File
@@ -19,14 +19,13 @@ class Call: ObservableObject, Equatable {
var direction: CallDirection
var contact: Contact
var callUUID: String?
var localMedia: CallMediaType
var initialCallType: CallMediaType
@Published var localMediaSources: CallMediaSources
@Published var callState: CallState
@Published var localCapabilities: CallCapabilities?
@Published var peerMedia: CallMediaType?
@Published var peerMediaSources: CallMediaSources = CallMediaSources()
@Published var sharedKey: String?
@Published var audioEnabled = true
@Published var speakerEnabled = false
@Published var videoEnabled: Bool
@Published var connectionInfo: ConnectionInfo?
@Published var connectedAt: Date? = nil
@@ -35,32 +34,33 @@ class Call: ObservableObject, Equatable {
contact: Contact,
callUUID: String?,
callState: CallState,
localMedia: CallMediaType,
initialCallType: CallMediaType,
sharedKey: String? = nil
) {
self.direction = direction
self.contact = contact
self.callUUID = callUUID
self.callState = callState
self.localMedia = localMedia
self.initialCallType = initialCallType
self.sharedKey = sharedKey
self.videoEnabled = localMedia == .video
self.localMediaSources = CallMediaSources(
mic: AVCaptureDevice.authorizationStatus(for: .audio) == .authorized,
camera: initialCallType == .video && AVCaptureDevice.authorizationStatus(for: .video) == .authorized)
}
var encrypted: Bool { get { localEncrypted && sharedKey != nil } }
var localEncrypted: Bool { get { localCapabilities?.encryption ?? false } }
private var localEncrypted: Bool { get { localCapabilities?.encryption ?? false } }
var encryptionStatus: LocalizedStringKey {
get {
switch callState {
case .waitCapabilities: return ""
case .invitationSent: return localEncrypted ? "e2e encrypted" : "no e2e encryption"
case .invitationAccepted: return sharedKey == nil ? "contact has no e2e encryption" : "contact has e2e encryption"
default: return !localEncrypted ? "no e2e encryption" : sharedKey == nil ? "contact has no e2e encryption" : "e2e encrypted"
default: return !localEncrypted ? "no e2e encryption" : sharedKey == nil ? "contact has no e2e encryption" : "e2e encrypted"
}
}
}
var hasMedia: Bool { get { callState == .offerSent || callState == .negotiated || callState == .connected } }
var supportsVideo: Bool { get { peerMedia == .video || localMedia == .video } }
var hasVideo: Bool { get { localMediaSources.hasVideo || peerMediaSources.hasVideo } }
}
enum CallDirection {
@@ -105,18 +105,28 @@ struct WVAPIMessage: Equatable, Decodable, Encodable {
var command: WCallCommand?
}
struct CallMediaSources: Equatable, Codable {
var mic: Bool = false
var camera: Bool = false
var screenAudio: Bool = false
var screenVideo: Bool = false
var hasVideo: Bool { get { camera || screenVideo } }
}
enum WCallCommand: Equatable, Encodable, Decodable {
case capabilities(media: CallMediaType)
case start(media: CallMediaType, aesKey: String? = nil, iceServers: [RTCIceServer]? = nil, relay: Bool? = nil)
case offer(offer: String, iceCandidates: String, media: CallMediaType, aesKey: String? = nil, iceServers: [RTCIceServer]? = nil, relay: Bool? = nil)
case answer(answer: String, iceCandidates: String)
case ice(iceCandidates: String)
case media(media: CallMediaType, enable: Bool)
case media(source: CallMediaSource, enable: Bool)
case end
enum CodingKeys: String, CodingKey {
case type
case media
case source
case aesKey
case offer
case answer
@@ -167,9 +177,9 @@ enum WCallCommand: Equatable, Encodable, Decodable {
case let .ice(iceCandidates):
try container.encode("ice", forKey: .type)
try container.encode(iceCandidates, forKey: .iceCandidates)
case let .media(media, enable):
case let .media(source, enable):
try container.encode("media", forKey: .type)
try container.encode(media, forKey: .media)
try container.encode(source, forKey: .media)
try container.encode(enable, forKey: .enable)
case .end:
try container.encode("end", forKey: .type)
@@ -205,9 +215,9 @@ enum WCallCommand: Equatable, Encodable, Decodable {
let iceCandidates = try container.decode(String.self, forKey: CodingKeys.iceCandidates)
self = .ice(iceCandidates: iceCandidates)
case "media":
let media = try container.decode(CallMediaType.self, forKey: CodingKeys.media)
let source = try container.decode(CallMediaSource.self, forKey: CodingKeys.source)
let enable = try container.decode(Bool.self, forKey: CodingKeys.enable)
self = .media(media: media, enable: enable)
self = .media(source: source, enable: enable)
case "end":
self = .end
default:
@@ -224,6 +234,7 @@ enum WCallResponse: Equatable, Decodable {
case ice(iceCandidates: String)
case connection(state: ConnectionState)
case connected(connectionInfo: ConnectionInfo)
case peerMedia(source: CallMediaSource, enabled: Bool)
case ended
case ok
case error(message: String)
@@ -238,6 +249,8 @@ enum WCallResponse: Equatable, Decodable {
case state
case connectionInfo
case message
case source
case enabled
}
var respType: String {
@@ -249,6 +262,7 @@ enum WCallResponse: Equatable, Decodable {
case .ice: return "ice"
case .connection: return "connection"
case .connected: return "connected"
case .peerMedia: return "peerMedia"
case .ended: return "ended"
case .ok: return "ok"
case .error: return "error"
@@ -283,6 +297,10 @@ enum WCallResponse: Equatable, Decodable {
case "connected":
let connectionInfo = try container.decode(ConnectionInfo.self, forKey: CodingKeys.connectionInfo)
self = .connected(connectionInfo: connectionInfo)
case "peerMedia":
let source = try container.decode(CallMediaSource.self, forKey: CodingKeys.source)
let enabled = try container.decode(Bool.self, forKey: CodingKeys.enabled)
self = .peerMedia(source: source, enabled: enabled)
case "ended":
self = .ended
case "ok":
@@ -324,6 +342,10 @@ extension WCallResponse: Encodable {
case let .connected(connectionInfo):
try container.encode("connected", forKey: .type)
try container.encode(connectionInfo, forKey: .connectionInfo)
case let .peerMedia(source, enabled):
try container.encode("peerMedia", forKey: .type)
try container.encode(source, forKey: .source)
try container.encode(enabled, forKey: .enabled)
case .ended:
try container.encode("ended", forKey: .type)
case .ok:
@@ -376,7 +398,7 @@ actor WebRTCCommandProcessor {
func shouldRunCommand(_ client: WebRTCClient, _ c: WCallCommand) -> Bool {
switch c {
case .capabilities, .start, .offer, .end: true
default: client.activeCall.wrappedValue != nil
default: client.activeCall != nil
}
}
}
+414 -103
View File
@@ -23,15 +23,24 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
struct Call {
var connection: RTCPeerConnection
var iceCandidates: IceCandidates
var localMedia: CallMediaType
var localCamera: RTCVideoCapturer?
var localVideoSource: RTCVideoSource?
var localStream: RTCVideoTrack?
var remoteStream: RTCVideoTrack?
var device: AVCaptureDevice.Position = .front
var localAudioTrack: RTCAudioTrack?
var localVideoTrack: RTCVideoTrack?
var remoteAudioTrack: RTCAudioTrack?
var remoteVideoTrack: RTCVideoTrack?
var remoteScreenAudioTrack: RTCAudioTrack?
var remoteScreenVideoTrack: RTCVideoTrack?
var device: AVCaptureDevice.Position
var aesKey: String?
var frameEncryptor: RTCFrameEncryptor?
var frameDecryptor: RTCFrameDecryptor?
var peerHasOldVersion: Bool
}
struct NotConnectedCall {
var audioTrack: RTCAudioTrack?
var localCameraAndTrack: (RTCVideoCapturer, RTCVideoTrack)?
var device: AVCaptureDevice.Position = .front
}
actor IceCandidates {
@@ -51,17 +60,20 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
private let rtcAudioSession = RTCAudioSession.sharedInstance()
private let audioQueue = DispatchQueue(label: "chat.simplex.app.audio")
private var sendCallResponse: (WVAPIMessage) async -> Void
var activeCall: Binding<Call?>
var activeCall: Call?
var notConnectedCall: NotConnectedCall?
private var localRendererAspectRatio: Binding<CGFloat?>
var cameraRenderers: [RTCVideoRenderer] = []
var screenRenderers: [RTCVideoRenderer] = []
@available(*, unavailable)
override init() {
fatalError("Unimplemented")
}
required init(_ activeCall: Binding<Call?>, _ sendCallResponse: @escaping (WVAPIMessage) async -> Void, _ localRendererAspectRatio: Binding<CGFloat?>) {
required init(_ sendCallResponse: @escaping (WVAPIMessage) async -> Void, _ localRendererAspectRatio: Binding<CGFloat?>) {
self.sendCallResponse = sendCallResponse
self.activeCall = activeCall
self.localRendererAspectRatio = localRendererAspectRatio
rtcAudioSession.useManualAudio = CallController.useCallKit()
rtcAudioSession.isAudioEnabled = !CallController.useCallKit()
@@ -78,39 +90,45 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
func initializeCall(_ iceServers: [WebRTC.RTCIceServer]?, _ mediaType: CallMediaType, _ aesKey: String?, _ relay: Bool?) -> Call {
let connection = createPeerConnection(iceServers ?? getWebRTCIceServers() ?? defaultIceServers, relay)
connection.delegate = self
createAudioSender(connection)
var localStream: RTCVideoTrack? = nil
var remoteStream: RTCVideoTrack? = nil
let device = notConnectedCall?.device ?? .front
var localCamera: RTCVideoCapturer? = nil
var localVideoSource: RTCVideoSource? = nil
if mediaType == .video {
(localStream, remoteStream, localCamera, localVideoSource) = createVideoSender(connection)
var localAudioTrack: RTCAudioTrack? = nil
var localVideoTrack: RTCVideoTrack? = nil
if let localCameraAndTrack = notConnectedCall?.localCameraAndTrack {
(localCamera, localVideoTrack) = localCameraAndTrack
} else if notConnectedCall == nil && mediaType == .video {
(localCamera, localVideoTrack) = createVideoTrackAndStartCapture(device)
}
if let audioTrack = notConnectedCall?.audioTrack {
localAudioTrack = audioTrack
} else if notConnectedCall == nil {
localAudioTrack = createAudioTrack()
}
notConnectedCall?.localCameraAndTrack = nil
notConnectedCall?.audioTrack = nil
var frameEncryptor: RTCFrameEncryptor? = nil
var frameDecryptor: RTCFrameDecryptor? = nil
if aesKey != nil {
let encryptor = RTCFrameEncryptor.init(sizeChange: Int32(WebRTCClient.ivTagBytes))
encryptor.delegate = self
frameEncryptor = encryptor
connection.senders.forEach { $0.setRtcFrameEncryptor(encryptor) }
let decryptor = RTCFrameDecryptor.init(sizeChange: -Int32(WebRTCClient.ivTagBytes))
decryptor.delegate = self
frameDecryptor = decryptor
// Has no video receiver in outgoing call if applied here, see [peerConnection(_ connection: RTCPeerConnection, didChange newState]
// connection.receivers.forEach { $0.setRtcFrameDecryptor(decryptor) }
}
return Call(
connection: connection,
iceCandidates: IceCandidates(),
localMedia: mediaType,
localCamera: localCamera,
localVideoSource: localVideoSource,
localStream: localStream,
remoteStream: remoteStream,
localAudioTrack: localAudioTrack,
localVideoTrack: localVideoTrack,
device: device,
aesKey: aesKey,
frameEncryptor: frameEncryptor,
frameDecryptor: frameDecryptor
frameDecryptor: frameDecryptor,
peerHasOldVersion: false
)
}
@@ -151,18 +169,24 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
func sendCallCommand(command: WCallCommand) async {
var resp: WCallResponse? = nil
let pc = activeCall.wrappedValue?.connection
let pc = activeCall?.connection
switch command {
case .capabilities:
case let .capabilities(media): // outgoing
let localCameraAndTrack: (RTCVideoCapturer, RTCVideoTrack)? = media == .video
? createVideoTrackAndStartCapture(.front)
: nil
notConnectedCall = NotConnectedCall(audioTrack: createAudioTrack(), localCameraAndTrack: localCameraAndTrack, device: .front)
resp = .capabilities(capabilities: CallCapabilities(encryption: WebRTCClient.enableEncryption))
case let .start(media: media, aesKey, iceServers, relay):
case let .start(media: media, aesKey, iceServers, relay): // incoming
logger.debug("starting incoming call - create webrtc session")
if activeCall.wrappedValue != nil { endCall() }
if activeCall != nil { endCall() }
let encryption = WebRTCClient.enableEncryption
let call = initializeCall(iceServers?.toWebRTCIceServers(), media, encryption ? aesKey : nil, relay)
activeCall.wrappedValue = call
activeCall = call
setupLocalTracks(true, call)
let (offer, error) = await call.connection.offer()
if let offer = offer {
setupEncryptionForLocalTracks(call)
resp = .offer(
offer: compressToBase64(input: encodeJSON(CustomRTCSessionDescription(type: offer.type.toSdpType(), sdp: offer.sdp))),
iceCandidates: compressToBase64(input: encodeJSON(await self.getInitialIceCandidates())),
@@ -172,18 +196,24 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
} else {
resp = .error(message: "offer error: \(error?.localizedDescription ?? "unknown error")")
}
case let .offer(offer, iceCandidates, media, aesKey, iceServers, relay):
if activeCall.wrappedValue != nil {
case let .offer(offer, iceCandidates, media, aesKey, iceServers, relay): // outgoing
if activeCall != nil {
resp = .error(message: "accept: call already started")
} else if !WebRTCClient.enableEncryption && aesKey != nil {
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(), media, WebRTCClient.enableEncryption ? aesKey : nil, relay)
activeCall.wrappedValue = call
activeCall = 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 {
setupLocalTracks(false, call)
setupEncryptionForLocalTracks(call)
pc.transceivers.forEach { transceiver in
transceiver.setDirection(.sendRecv, error: nil)
}
await adaptToOldVersion(pc.transceivers.count <= 2)
let (answer, error) = await pc.answer()
if let answer = answer {
self.addIceCandidates(pc, remoteIceCandidates)
@@ -200,7 +230,7 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
}
}
}
case let .answer(answer, iceCandidates):
case let .answer(answer, iceCandidates): // incoming
if pc == nil {
resp = .error(message: "answer: call not started")
} else if pc?.localDescription == nil {
@@ -212,6 +242,9 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
let type = answer.type, let sdp = answer.sdp,
let pc = pc {
if (try? await pc.setRemoteDescription(RTCSessionDescription(type: type.toWebRTCSdpType(), sdp: sdp))) != nil {
var currentDirection: RTCRtpTransceiverDirection = .sendOnly
pc.transceivers[2].currentDirection(&currentDirection)
await adaptToOldVersion(currentDirection == .sendOnly)
addIceCandidates(pc, remoteIceCandidates)
resp = .ok
} else {
@@ -226,13 +259,11 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
} else {
resp = .error(message: "ice: call not started")
}
case let .media(media, enable):
if activeCall.wrappedValue == nil {
case let .media(source, enable):
if activeCall == nil {
resp = .error(message: "media: call not started")
} else if activeCall.wrappedValue?.localMedia == .audio && media == .video {
resp = .error(message: "media: no video")
} else {
enableMedia(media, enable)
await enableMedia(source, enable)
resp = .ok
}
case .end:
@@ -247,7 +278,7 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
func getInitialIceCandidates() async -> [RTCIceCandidate] {
await untilIceComplete(timeoutMs: 750, stepMs: 150) {}
let candidates = await activeCall.wrappedValue?.iceCandidates.getAndClear() ?? []
let candidates = await activeCall?.iceCandidates.getAndClear() ?? []
logger.debug("WebRTCClient: sending initial ice candidates: \(candidates.count)")
return candidates
}
@@ -255,7 +286,7 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
func waitForMoreIceCandidates() {
Task {
await untilIceComplete(timeoutMs: 12000, stepMs: 1500) {
let candidates = await self.activeCall.wrappedValue?.iceCandidates.getAndClear() ?? []
let candidates = await self.activeCall?.iceCandidates.getAndClear() ?? []
if candidates.count > 0 {
logger.debug("WebRTCClient: sending more ice candidates: \(candidates.count)")
await self.sendIceCandidates(candidates)
@@ -272,25 +303,202 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
)
}
func enableMedia(_ media: CallMediaType, _ enable: Bool) {
logger.debug("WebRTCClient: enabling media \(media.rawValue) \(enable)")
media == .video ? setVideoEnabled(enable) : setAudioEnabled(enable)
func setupMuteUnmuteListener(_ transceiver: RTCRtpTransceiver, _ track: RTCMediaStreamTrack) {
// logger.log("Setting up mute/unmute listener in the call without encryption for mid = \(transceiver.mid)")
Task {
var lastBytesReceived: Int64 = 0
// muted initially
var mutedSeconds = 4
while let call = self.activeCall, transceiver.receiver.track?.readyState == .live {
let stats: RTCStatisticsReport = await call.connection.statistics(for: transceiver.receiver)
let stat = stats.statistics.values.first(where: { stat in stat.type == "inbound-rtp"})
if let stat {
//logger.debug("Stat \(stat.debugDescription)")
let bytes = stat.values["bytesReceived"] as! Int64
if bytes <= lastBytesReceived {
mutedSeconds += 1
if mutedSeconds == 3 {
await MainActor.run {
self.onMediaMuteUnmute(transceiver.mid, true)
}
}
} else {
if mutedSeconds >= 3 {
await MainActor.run {
self.onMediaMuteUnmute(transceiver.mid, false)
}
}
lastBytesReceived = bytes
mutedSeconds = 0
}
}
try? await Task.sleep(nanoseconds: 1000_000000)
}
}
}
func addLocalRenderer(_ activeCall: Call, _ renderer: RTCEAGLVideoView) {
activeCall.localStream?.add(renderer)
@MainActor
func onMediaMuteUnmute(_ transceiverMid: String?, _ mute: Bool) {
guard let activeCall = ChatModel.shared.activeCall else { return }
let source = mediaSourceFromTransceiverMid(transceiverMid)
logger.log("Mute/unmute \(source.rawValue) track = \(mute) with mid = \(transceiverMid ?? "nil")")
if source == .mic && activeCall.peerMediaSources.mic == mute {
activeCall.peerMediaSources.mic = !mute
} else if (source == .camera && activeCall.peerMediaSources.camera == mute) {
activeCall.peerMediaSources.camera = !mute
} else if (source == .screenAudio && activeCall.peerMediaSources.screenAudio == mute) {
activeCall.peerMediaSources.screenAudio = !mute
} else if (source == .screenVideo && activeCall.peerMediaSources.screenVideo == mute) {
activeCall.peerMediaSources.screenVideo = !mute
}
}
@MainActor
func enableMedia(_ source: CallMediaSource, _ enable: Bool) {
logger.debug("WebRTCClient: enabling media \(source.rawValue) \(enable)")
source == .camera ? setCameraEnabled(enable) : setAudioEnabled(enable)
}
@MainActor
func adaptToOldVersion(_ peerHasOldVersion: Bool) {
activeCall?.peerHasOldVersion = peerHasOldVersion
if peerHasOldVersion {
logger.debug("The peer has an old version. Remote audio track is nil = \(self.activeCall?.remoteAudioTrack == nil), video = \(self.activeCall?.remoteVideoTrack == nil)")
onMediaMuteUnmute("0", false)
if activeCall?.remoteVideoTrack != nil {
onMediaMuteUnmute("1", false)
}
if ChatModel.shared.activeCall?.localMediaSources.camera == true && ChatModel.shared.activeCall?.peerMediaSources.camera == false {
logger.debug("Stopping video track for the old version")
activeCall?.connection.senders[1].track = nil
ChatModel.shared.activeCall?.localMediaSources.camera = false
(activeCall?.localCamera as? RTCCameraVideoCapturer)?.stopCapture()
activeCall?.localCamera = nil
activeCall?.localVideoTrack = nil
}
}
}
func addLocalRenderer(_ renderer: RTCEAGLVideoView) {
if let activeCall {
if let track = activeCall.localVideoTrack {
track.add(renderer)
}
} else if let notConnectedCall {
if let track = notConnectedCall.localCameraAndTrack?.1 {
track.add(renderer)
}
}
// To get width and height of a frame, see videoView(videoView:, didChangeVideoSize)
renderer.delegate = self
}
func removeLocalRenderer(_ renderer: RTCEAGLVideoView) {
if let activeCall {
if let track = activeCall.localVideoTrack {
track.remove(renderer)
}
} else if let notConnectedCall {
if let track = notConnectedCall.localCameraAndTrack?.1 {
track.remove(renderer)
}
}
renderer.delegate = nil
}
func videoView(_ videoView: RTCVideoRenderer, didChangeVideoSize size: CGSize) {
guard size.height > 0 else { return }
localRendererAspectRatio.wrappedValue = size.width / size.height
}
func setupLocalTracks(_ incomingCall: Bool, _ call: Call) {
let pc = call.connection
let transceivers = call.connection.transceivers
let audioTrack = call.localAudioTrack
let videoTrack = call.localVideoTrack
if incomingCall {
let micCameraInit = RTCRtpTransceiverInit()
// streamIds required for old versions which adds tracks from stream, not from track property
micCameraInit.streamIds = ["micCamera"]
let screenAudioVideoInit = RTCRtpTransceiverInit()
screenAudioVideoInit.streamIds = ["screenAudioVideo"]
// incoming call, no transceivers yet. But they should be added in order: mic, camera, screen audio, screen video
// mid = 0, mic
if let audioTrack {
pc.addTransceiver(with: audioTrack, init: micCameraInit)
} else {
pc.addTransceiver(of: .audio, init: micCameraInit)
}
// mid = 1, camera
if let videoTrack {
pc.addTransceiver(with: videoTrack, init: micCameraInit)
} else {
pc.addTransceiver(of: .video, init: micCameraInit)
}
// mid = 2, screenAudio
pc.addTransceiver(of: .audio, init: screenAudioVideoInit)
// mid = 3, screenVideo
pc.addTransceiver(of: .video, init: screenAudioVideoInit)
} else {
// new version
if transceivers.count > 2 {
// Outgoing call. All transceivers are ready. Don't addTrack() because it will create new transceivers, replace existing (nil) tracks
transceivers
.first(where: { elem in mediaSourceFromTransceiverMid(elem.mid) == .mic })?
.sender.track = audioTrack
transceivers
.first(where: { elem in mediaSourceFromTransceiverMid(elem.mid) == .camera })?
.sender.track = videoTrack
} else {
// old version, only two transceivers
if let audioTrack {
pc.add(audioTrack, streamIds: ["micCamera"])
} else {
// it's important to have any track in order to be able to turn it on again (currently it's off)
let sender = pc.add(createAudioTrack(), streamIds: ["micCamera"])
sender?.track = nil
}
if let videoTrack {
pc.add(videoTrack, streamIds: ["micCamera"])
} else {
// it's important to have any track in order to be able to turn it on again (currently it's off)
let localVideoSource = WebRTCClient.factory.videoSource()
let localVideoTrack = WebRTCClient.factory.videoTrack(with: localVideoSource, trackId: "video0")
let sender = pc.add(localVideoTrack, streamIds: ["micCamera"])
sender?.track = nil
}
}
}
}
func mediaSourceFromTransceiverMid(_ mid: String?) -> CallMediaSource {
switch mid {
case "0":
return .mic
case "1":
return .camera
case "2":
return .screenAudio
case "3":
return .screenVideo
default:
return .unknown
}
}
// Should be called after local description set
func setupEncryptionForLocalTracks(_ call: Call) {
if let encryptor = call.frameEncryptor {
call.connection.senders.forEach { $0.setRtcFrameEncryptor(encryptor) }
}
}
func frameDecryptor(_ decryptor: RTCFrameDecryptor, mediaType: RTCRtpMediaType, withFrame encrypted: Data) -> Data? {
guard encrypted.count > 0 else { return nil }
if var key: [CChar] = activeCall.wrappedValue?.aesKey?.cString(using: .utf8),
if var key: [CChar] = activeCall?.aesKey?.cString(using: .utf8),
let pointer: UnsafeMutableRawPointer = malloc(encrypted.count) {
memcpy(pointer, (encrypted as NSData).bytes, encrypted.count)
let isKeyFrame = encrypted[0] & 1 == 0
@@ -304,7 +512,7 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
func frameEncryptor(_ encryptor: RTCFrameEncryptor, mediaType: RTCRtpMediaType, withFrame unencrypted: Data) -> Data? {
guard unencrypted.count > 0 else { return nil }
if var key: [CChar] = activeCall.wrappedValue?.aesKey?.cString(using: .utf8),
if var key: [CChar] = activeCall?.aesKey?.cString(using: .utf8),
let pointer: UnsafeMutableRawPointer = malloc(unencrypted.count + WebRTCClient.ivTagBytes) {
memcpy(pointer, (unencrypted as NSData).bytes, unencrypted.count)
let isKeyFrame = unencrypted[0] & 1 == 0
@@ -327,18 +535,42 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
}
}
func addRemoteRenderer(_ activeCall: Call, _ renderer: RTCVideoRenderer) {
activeCall.remoteStream?.add(renderer)
func addRemoteCameraRenderer(_ renderer: RTCVideoRenderer) {
if activeCall?.remoteVideoTrack != nil {
activeCall?.remoteVideoTrack?.add(renderer)
} else {
cameraRenderers.append(renderer)
}
}
func removeRemoteRenderer(_ activeCall: Call, _ renderer: RTCVideoRenderer) {
activeCall.remoteStream?.remove(renderer)
func removeRemoteCameraRenderer(_ renderer: RTCVideoRenderer) {
if activeCall?.remoteVideoTrack != nil {
activeCall?.remoteVideoTrack?.remove(renderer)
} else {
cameraRenderers.removeAll(where: { $0.isEqual(renderer) })
}
}
func startCaptureLocalVideo(_ activeCall: Call) {
func addRemoteScreenRenderer(_ renderer: RTCVideoRenderer) {
if activeCall?.remoteScreenVideoTrack != nil {
activeCall?.remoteScreenVideoTrack?.add(renderer)
} else {
screenRenderers.append(renderer)
}
}
func removeRemoteScreenRenderer(_ renderer: RTCVideoRenderer) {
if activeCall?.remoteScreenVideoTrack != nil {
activeCall?.remoteScreenVideoTrack?.remove(renderer)
} else {
screenRenderers.removeAll(where: { $0.isEqual(renderer) })
}
}
func startCaptureLocalVideo(_ device: AVCaptureDevice.Position?, _ capturer: RTCVideoCapturer?) {
#if targetEnvironment(simulator)
guard
let capturer = activeCall.localCamera as? RTCFileVideoCapturer
let capturer = (activeCall?.localCamera ?? notConnectedCall?.localCameraAndTrack?.0) as? RTCFileVideoCapturer
else {
logger.error("Unable to work with a file capturer")
return
@@ -348,10 +580,10 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
capturer.startCapturing(fromFileNamed: "sounds/video.mp4")
#else
guard
let capturer = activeCall.localCamera as? RTCCameraVideoCapturer,
let camera = (RTCCameraVideoCapturer.captureDevices().first { $0.position == activeCall.device })
let capturer = capturer as? RTCCameraVideoCapturer,
let camera = (RTCCameraVideoCapturer.captureDevices().first { $0.position == device })
else {
logger.error("Unable to find a camera")
logger.error("Unable to find a camera or local track")
return
}
@@ -377,19 +609,6 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
#endif
}
private func createAudioSender(_ connection: RTCPeerConnection) {
let streamId = "stream"
let audioTrack = createAudioTrack()
connection.add(audioTrack, streamIds: [streamId])
}
private func createVideoSender(_ connection: RTCPeerConnection) -> (RTCVideoTrack?, RTCVideoTrack?, RTCVideoCapturer?, RTCVideoSource?) {
let streamId = "stream"
let (localVideoTrack, localCamera, localVideoSource) = createVideoTrack()
connection.add(localVideoTrack, streamIds: [streamId])
return (localVideoTrack, connection.transceivers.first { $0.mediaType == .video }?.receiver.track as? RTCVideoTrack, localCamera, localVideoSource)
}
private func createAudioTrack() -> RTCAudioTrack {
let audioConstrains = RTCMediaConstraints(mandatoryConstraints: nil, optionalConstraints: nil)
let audioSource = WebRTCClient.factory.audioSource(with: audioConstrains)
@@ -397,7 +616,7 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
return audioTrack
}
private func createVideoTrack() -> (RTCVideoTrack, RTCVideoCapturer, RTCVideoSource) {
private func createVideoTrackAndStartCapture(_ device: AVCaptureDevice.Position) -> (RTCVideoCapturer, RTCVideoTrack) {
let localVideoSource = WebRTCClient.factory.videoSource()
#if targetEnvironment(simulator)
@@ -407,7 +626,8 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
#endif
let localVideoTrack = WebRTCClient.factory.videoTrack(with: localVideoSource, trackId: "video0")
return (localVideoTrack, localCamera, localVideoSource)
startCaptureLocalVideo(device, localCamera)
return (localCamera, localVideoTrack)
}
func endCall() {
@@ -420,15 +640,16 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
}
private func _endCall() {
guard let call = activeCall.wrappedValue else { return }
(notConnectedCall?.localCameraAndTrack?.0 as? RTCCameraVideoCapturer)?.stopCapture()
guard let call = activeCall else { return }
logger.debug("WebRTCClient: ending the call")
activeCall.wrappedValue = nil
(call.localCamera as? RTCCameraVideoCapturer)?.stopCapture()
call.connection.close()
call.connection.delegate = nil
call.frameEncryptor?.delegate = nil
call.frameDecryptor?.delegate = nil
(call.localCamera as? RTCCameraVideoCapturer)?.stopCapture()
audioSessionToDefaults()
activeCall = nil
}
func untilIceComplete(timeoutMs: UInt64, stepMs: UInt64, action: @escaping () async -> Void) async {
@@ -437,7 +658,7 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
_ = try? await Task.sleep(nanoseconds: stepMs * 1000000)
t += stepMs
await action()
} while t < timeoutMs && activeCall.wrappedValue?.connection.iceGatheringState != .complete
} while t < timeoutMs && activeCall?.connection.iceGatheringState != .complete
}
}
@@ -498,11 +719,40 @@ extension WebRTCClient: RTCPeerConnectionDelegate {
logger.debug("Connection should negotiate")
}
func peerConnection(_ peerConnection: RTCPeerConnection, didStartReceivingOn transceiver: RTCRtpTransceiver) {
if let track = transceiver.receiver.track {
DispatchQueue.main.async {
// Doesn't work for outgoing video call (audio in video call works ok still, same as incoming call)
// if let decryptor = self.activeCall?.frameDecryptor {
// transceiver.receiver.setRtcFrameDecryptor(decryptor)
// }
let source = self.mediaSourceFromTransceiverMid(transceiver.mid)
switch source {
case .mic: self.activeCall?.remoteAudioTrack = track as? RTCAudioTrack
case .camera:
self.activeCall?.remoteVideoTrack = track as? RTCVideoTrack
self.cameraRenderers.forEach({ renderer in
self.activeCall?.remoteVideoTrack?.add(renderer)
})
self.cameraRenderers.removeAll()
case .screenAudio: self.activeCall?.remoteScreenAudioTrack = track as? RTCAudioTrack
case .screenVideo:
self.activeCall?.remoteScreenVideoTrack = track as? RTCVideoTrack
self.screenRenderers.forEach({ renderer in
self.activeCall?.remoteScreenVideoTrack?.add(renderer)
})
self.screenRenderers.removeAll()
case .unknown: ()
}
}
self.setupMuteUnmuteListener(transceiver, track)
}
}
func peerConnection(_ connection: RTCPeerConnection, didChange newState: RTCIceConnectionState) {
debugPrint("Connection new connection state: \(newState.toString() ?? "" + newState.rawValue.description) \(connection.receivers)")
guard let call = activeCall.wrappedValue,
let connectionStateString = newState.toString(),
guard let connectionStateString = newState.toString(),
let iceConnectionStateString = connection.iceConnectionState.toString(),
let iceGatheringStateString = connection.iceGatheringState.toString(),
let signalingStateString = connection.signalingState.toString()
@@ -523,18 +773,14 @@ extension WebRTCClient: RTCPeerConnectionDelegate {
switch newState {
case .checking:
if let frameDecryptor = activeCall.wrappedValue?.frameDecryptor {
if let frameDecryptor = activeCall?.frameDecryptor {
connection.receivers.forEach { $0.setRtcFrameDecryptor(frameDecryptor) }
}
let enableSpeaker: Bool
switch call.localMedia {
case .video: enableSpeaker = true
default: enableSpeaker = false
}
let enableSpeaker: Bool = ChatModel.shared.activeCall?.localMediaSources.hasVideo == true
setSpeakerEnabledAndConfigureSession(enableSpeaker)
case .connected: sendConnectedEvent(connection)
case .disconnected, .failed: endCall()
default: do {}
default: ()
}
}
}
@@ -546,7 +792,7 @@ extension WebRTCClient: RTCPeerConnectionDelegate {
func peerConnection(_ connection: RTCPeerConnection, didGenerate candidate: WebRTC.RTCIceCandidate) {
// logger.debug("Connection generated candidate \(candidate.debugDescription)")
Task {
await self.activeCall.wrappedValue?.iceCandidates.append(candidate.toCandidate(nil, nil))
await self.activeCall?.iceCandidates.append(candidate.toCandidate(nil, nil))
}
}
@@ -601,11 +847,42 @@ extension WebRTCClient: RTCPeerConnectionDelegate {
}
extension WebRTCClient {
func setAudioEnabled(_ enabled: Bool) {
setTrackEnabled(RTCAudioTrack.self, enabled)
static func isAuthorized(for type: AVMediaType) async -> Bool {
let status = AVCaptureDevice.authorizationStatus(for: type)
var isAuthorized = status == .authorized
if status == .notDetermined {
isAuthorized = await AVCaptureDevice.requestAccess(for: type)
}
return isAuthorized
}
func setSpeakerEnabledAndConfigureSession( _ enabled: Bool) {
static func showUnauthorizedAlert(for type: AVMediaType) {
if type == .audio {
AlertManager.shared.showAlert(Alert(
title: Text("No permission to record speech"),
message: Text("To record speech please grant permission to use Microphone."),
primaryButton: .default(Text("Open Settings")) {
DispatchQueue.main.async {
UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!, options: [:], completionHandler: nil)
}
},
secondaryButton: .cancel()
))
} else if type == .video {
AlertManager.shared.showAlert(Alert(
title: Text("No permission to record video"),
message: Text("To record video please grant permission to use Camera."),
primaryButton: .default(Text("Open Settings")) {
DispatchQueue.main.async {
UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!, options: [:], completionHandler: nil)
}
},
secondaryButton: .cancel()
))
}
}
func setSpeakerEnabledAndConfigureSession( _ enabled: Bool, skipExternalDevice: Bool = false) {
logger.debug("WebRTCClient: configuring session with speaker enabled \(enabled)")
audioQueue.async { [weak self] in
guard let self = self else { return }
@@ -618,7 +895,7 @@ extension WebRTCClient {
if enabled {
try self.rtcAudioSession.setCategory(AVAudioSession.Category.playAndRecord.rawValue, with: [.defaultToSpeaker, .allowBluetooth, .allowAirPlay, .allowBluetoothA2DP])
try self.rtcAudioSession.setMode(AVAudioSession.Mode.videoChat.rawValue)
if hasExternalAudioDevice, let preferred = self.rtcAudioSession.session.preferredInputDevice() {
if hasExternalAudioDevice && !skipExternalDevice, let preferred = self.rtcAudioSession.session.preferredInputDevice() {
try self.rtcAudioSession.setPreferredInput(preferred)
} else {
try self.rtcAudioSession.overrideOutputAudioPort(.speaker)
@@ -628,7 +905,7 @@ extension WebRTCClient {
try self.rtcAudioSession.setMode(AVAudioSession.Mode.voiceChat.rawValue)
try self.rtcAudioSession.overrideOutputAudioPort(.none)
}
if hasExternalAudioDevice {
if hasExternalAudioDevice && !skipExternalDevice {
logger.debug("WebRTCClient: configuring session with external device available, skip configuring speaker")
}
try self.rtcAudioSession.setActive(true)
@@ -659,25 +936,59 @@ extension WebRTCClient {
}
}
func setVideoEnabled(_ enabled: Bool) {
setTrackEnabled(RTCVideoTrack.self, enabled)
@MainActor
func setAudioEnabled(_ enabled: Bool) {
if activeCall != nil {
activeCall?.localAudioTrack = enabled ? createAudioTrack() : nil
activeCall?.connection.transceivers.first(where: { t in mediaSourceFromTransceiverMid(t.mid) == .mic })?.sender.track = activeCall?.localAudioTrack
} else if notConnectedCall != nil {
notConnectedCall?.audioTrack = enabled ? createAudioTrack() : nil
}
ChatModel.shared.activeCall?.localMediaSources.mic = enabled
}
@MainActor
func setCameraEnabled(_ enabled: Bool) {
if let call = activeCall {
if enabled {
if call.localVideoTrack == nil {
let device = activeCall?.device ?? notConnectedCall?.device ?? .front
let (camera, track) = createVideoTrackAndStartCapture(device)
activeCall?.localCamera = camera
activeCall?.localVideoTrack = track
}
} else {
(call.localCamera as? RTCCameraVideoCapturer)?.stopCapture()
activeCall?.localCamera = nil
activeCall?.localVideoTrack = nil
}
call.connection.transceivers
.first(where: { t in mediaSourceFromTransceiverMid(t.mid) == .camera })?
.sender.track = activeCall?.localVideoTrack
ChatModel.shared.activeCall?.localMediaSources.camera = activeCall?.localVideoTrack != nil
} else if let call = notConnectedCall {
if enabled {
let device = activeCall?.device ?? notConnectedCall?.device ?? .front
notConnectedCall?.localCameraAndTrack = createVideoTrackAndStartCapture(device)
} else {
(call.localCameraAndTrack?.0 as? RTCCameraVideoCapturer)?.stopCapture()
notConnectedCall?.localCameraAndTrack = nil
}
ChatModel.shared.activeCall?.localMediaSources.camera = notConnectedCall?.localCameraAndTrack != nil
}
}
func flipCamera() {
switch activeCall.wrappedValue?.device {
case .front: activeCall.wrappedValue?.device = .back
case .back: activeCall.wrappedValue?.device = .front
default: ()
let device = activeCall?.device ?? notConnectedCall?.device
if activeCall != nil {
activeCall?.device = device == .front ? .back : .front
} else {
notConnectedCall?.device = device == .front ? .back : .front
}
if let call = activeCall.wrappedValue {
startCaptureLocalVideo(call)
}
}
private func setTrackEnabled<T: RTCMediaStreamTrack>(_ type: T.Type, _ enabled: Bool) {
activeCall.wrappedValue?.connection.transceivers
.compactMap { $0.sender.track as? T }
.forEach { $0.isEnabled = enabled }
startCaptureLocalVideo(
activeCall?.device ?? notConnectedCall?.device,
(activeCall?.localCamera ?? notConnectedCall?.localCameraAndTrack?.0) as? RTCCameraVideoCapturer
)
}
}
@@ -45,7 +45,7 @@ struct ChatInfoToolbar: View {
}
private var contactVerifiedShield: Text {
(Text(Image(systemName: "checkmark.shield")) + Text(" "))
(Text(Image(systemName: "checkmark.shield")) + textSpace)
.font(.caption)
.foregroundColor(theme.colors.secondary)
.baselineOffset(1)
+106 -25
View File
@@ -96,6 +96,8 @@ struct ChatInfoView: View {
@ObservedObject var chat: Chat
@State var contact: Contact
@State var localAlias: String
@State var featuresAllowed: ContactFeaturesAllowed
@State var currentFeaturesAllowed: ContactFeaturesAllowed
var onSearch: () -> Void
@State private var connectionStats: ConnectionStats? = nil
@State private var customUserProfile: Profile? = nil
@@ -154,8 +156,8 @@ struct ChatInfoView: View {
HStack(alignment: .center, spacing: 8) {
let buttonWidth = g.size.width / 4
searchButton(width: buttonWidth)
AudioCallButton(chat: chat, contact: contact, width: buttonWidth) { alert = .someAlert(alert: $0) }
VideoButton(chat: chat, contact: contact, width: buttonWidth) { alert = .someAlert(alert: $0) }
AudioCallButton(chat: chat, contact: contact, connectionStats: $connectionStats, width: buttonWidth) { alert = .someAlert(alert: $0) }
VideoButton(chat: chat, contact: contact, connectionStats: $connectionStats, width: buttonWidth) { alert = .someAlert(alert: $0) }
muteButton(width: buttonWidth)
}
}
@@ -312,7 +314,15 @@ struct ChatInfoView: View {
case .networkStatusAlert: return networkStatusAlert()
case .switchAddressAlert: return switchAddressAlert(switchContactAddress)
case .abortSwitchAddressAlert: return abortSwitchAddressAlert(abortSwitchContactAddress)
case .syncConnectionForceAlert: return syncConnectionForceAlert({ syncContactConnection(force: true) })
case .syncConnectionForceAlert:
return syncConnectionForceAlert({
Task {
if let stats = await syncContactConnection(contact, force: true, showAlert: { alert = .someAlert(alert: $0) }) {
connectionStats = stats
dismiss()
}
}
})
case let .queueInfo(info): return queueInfoAlert(info)
case let .someAlert(a): return a.alert
case let .error(title, error): return mkAlert(title: title, message: error)
@@ -327,6 +337,16 @@ struct ChatInfoView: View {
$0.content
}
}
.onDisappear {
if currentFeaturesAllowed != featuresAllowed {
showAlert(
title: NSLocalizedString("Save preferences?", comment: "alert title"),
buttonTitle: NSLocalizedString("Save and notify contact", comment: "alert button"),
buttonAction: { savePreferences() },
cancelButton: true
)
}
}
}
private func contactInfoHeader() -> some View {
@@ -339,7 +359,7 @@ struct ChatInfoView: View {
Text(Image(systemName: "checkmark.shield"))
.foregroundColor(theme.colors.secondary)
.font(.title2)
+ Text(" ")
+ textSpace
+ Text(contact.profile.displayName)
.font(.largeTitle)
)
@@ -447,8 +467,9 @@ struct ChatInfoView: View {
NavigationLink {
ContactPreferencesView(
contact: $contact,
featuresAllowed: contactUserPrefsToFeaturesAllowed(contact.mergedPreferences),
currentFeaturesAllowed: contactUserPrefsToFeaturesAllowed(contact.mergedPreferences)
featuresAllowed: $featuresAllowed,
currentFeaturesAllowed: $currentFeaturesAllowed,
savePreferences: savePreferences
)
.navigationBarTitle("Contact preferences")
.modifier(ThemedBackground(grouped: true))
@@ -480,7 +501,12 @@ struct ChatInfoView: View {
private func synchronizeConnectionButton() -> some View {
Button {
syncContactConnection(force: false)
Task {
if let stats = await syncContactConnection(contact, force: false, showAlert: { alert = .someAlert(alert: $0) }) {
connectionStats = stats
dismiss()
}
}
} label: {
Label("Fix connection", systemImage: "exclamationmark.arrow.triangle.2.circlepath")
.foregroundColor(.orange)
@@ -599,29 +625,50 @@ struct ChatInfoView: View {
}
}
private func syncContactConnection(force: Bool) {
private func savePreferences() {
Task {
do {
let stats = try apiSyncContactRatchet(contact.apiId, force)
connectionStats = stats
await MainActor.run {
chatModel.updateContactConnectionStats(contact, stats)
dismiss()
}
} catch let error {
logger.error("syncContactConnection apiSyncContactRatchet error: \(responseError(error))")
let a = getErrorAlert(error, "Error synchronizing connection")
await MainActor.run {
alert = .error(title: a.title, error: a.message)
let prefs = contactFeaturesAllowedToPrefs(featuresAllowed)
if let toContact = try await apiSetContactPrefs(contactId: contact.contactId, preferences: prefs) {
await MainActor.run {
contact = toContact
chatModel.updateContact(toContact)
currentFeaturesAllowed = featuresAllowed
}
}
} catch {
logger.error("ContactPreferencesView apiSetContactPrefs error: \(responseError(error))")
}
}
}
}
func syncContactConnection(_ contact: Contact, force: Bool, showAlert: (SomeAlert) -> Void) async -> ConnectionStats? {
do {
let stats = try apiSyncContactRatchet(contact.apiId, force)
await MainActor.run {
ChatModel.shared.updateContactConnectionStats(contact, stats)
}
return stats
} catch let error {
logger.error("syncContactConnection apiSyncContactRatchet error: \(responseError(error))")
let a = getErrorAlert(error, "Error synchronizing connection")
await MainActor.run {
showAlert(
SomeAlert(
alert: mkAlert(title: a.title, message: a.message),
id: "syncContactConnection error"
)
)
}
return nil
}
}
struct AudioCallButton: View {
var chat: Chat
var contact: Contact
@Binding var connectionStats: ConnectionStats?
var width: CGFloat
var showAlert: (SomeAlert) -> Void
@@ -629,6 +676,7 @@ struct AudioCallButton: View {
CallButton(
chat: chat,
contact: contact,
connectionStats: $connectionStats,
image: "phone.fill",
title: "call",
mediaType: .audio,
@@ -641,6 +689,7 @@ struct AudioCallButton: View {
struct VideoButton: View {
var chat: Chat
var contact: Contact
@Binding var connectionStats: ConnectionStats?
var width: CGFloat
var showAlert: (SomeAlert) -> Void
@@ -648,6 +697,7 @@ struct VideoButton: View {
CallButton(
chat: chat,
contact: contact,
connectionStats: $connectionStats,
image: "video.fill",
title: "video",
mediaType: .video,
@@ -660,6 +710,7 @@ struct VideoButton: View {
private struct CallButton: View {
var chat: Chat
var contact: Contact
@Binding var connectionStats: ConnectionStats?
var image: String
var title: LocalizedStringKey
var mediaType: CallMediaType
@@ -671,12 +722,40 @@ private struct CallButton: View {
InfoViewButton(image: image, title: title, disabledLook: !canCall, width: width) {
if canCall {
if CallController.useCallKit() {
CallController.shared.startCall(contact, mediaType)
} else {
// When CallKit is not used, colorscheme will be changed and it will be visible if not hiding sheets first
dismissAllSheets(animated: true) {
CallController.shared.startCall(contact, mediaType)
if let connStats = connectionStats {
if connStats.ratchetSyncState == .ok {
if CallController.useCallKit() {
CallController.shared.startCall(contact, mediaType)
} else {
// When CallKit is not used, colorscheme will be changed and it will be visible if not hiding sheets first
dismissAllSheets(animated: true) {
CallController.shared.startCall(contact, mediaType)
}
}
} else if connStats.ratchetSyncAllowed {
showAlert(SomeAlert(
alert: Alert(
title: Text("Fix connection?"),
message: Text("Connection requires encryption renegotiation."),
primaryButton: .default(Text("Fix")) {
Task {
if let stats = await syncContactConnection(contact, force: false, showAlert: showAlert) {
connectionStats = stats
}
}
},
secondaryButton: .cancel()
),
id: "can't call contact, fix connection"
))
} else {
showAlert(SomeAlert(
alert: mkAlert(
title: "Can't call contact",
message: "Encryption renegotiation in progress."
),
id: "can't call contact, encryption renegotiation in progress"
))
}
}
} else if contact.nextSendGrpInv {
@@ -1173,6 +1252,8 @@ struct ChatInfoView_Previews: PreviewProvider {
chat: Chat(chatInfo: ChatInfo.sampleData.direct, chatItems: []),
contact: Contact.sampleData,
localAlias: "",
featuresAllowed: contactUserPrefsToFeaturesAllowed(Contact.sampleData.mergedPreferences),
currentFeaturesAllowed: contactUserPrefsToFeaturesAllowed(Contact.sampleData.mergedPreferences),
onSearch: {}
)
}
@@ -11,11 +11,11 @@ import SimpleXChat
struct CIChatFeatureView: View {
@EnvironmentObject var m: ChatModel
@Environment(\.revealed) var revealed: Bool
@ObservedObject var im = ItemsModel.shared
@ObservedObject var chat: Chat
@EnvironmentObject var theme: AppTheme
var chatItem: ChatItem
@Binding var revealed: Bool
var feature: Feature
var icon: String? = nil
var iconColor: Color
@@ -106,6 +106,9 @@ struct CIChatFeatureView: View {
struct CIChatFeatureView_Previews: PreviewProvider {
static var previews: some View {
let enabled = FeatureEnabled(forUser: false, forContact: false)
CIChatFeatureView(chat: Chat.sampleData, chatItem: ChatItem.getChatFeatureSample(.fullDelete, enabled), revealed: Binding.constant(true), feature: ChatFeature.fullDelete, iconColor: enabled.iconColor(.secondary))
CIChatFeatureView(
chat: Chat.sampleData,
chatItem: ChatItem.getChatFeatureSample(.fullDelete, enabled), feature: ChatFeature.fullDelete, iconColor: enabled.iconColor(.secondary)
).environment(\.revealed, true)
}
}
@@ -47,7 +47,7 @@ struct CIFeaturePreferenceView: View {
+ Text(acceptText)
.fontWeight(.medium)
.foregroundColor(theme.colors.primary)
+ Text(" ")
+ Text(verbatim: " ")
}
r = r + chatItem.timestampText
.fontWeight(.light)
@@ -285,17 +285,18 @@ struct CIFileView_Previews: PreviewProvider {
file: nil
)
Group {
ChatItemView(chat: Chat.sampleData, chatItem: sentFile, revealed: Binding.constant(false))
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getFileMsgContentSample(), revealed: Binding.constant(false))
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getFileMsgContentSample(fileName: "some_long_file_name_here", fileStatus: .rcvInvitation), revealed: Binding.constant(false))
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getFileMsgContentSample(fileStatus: .rcvAccepted), revealed: Binding.constant(false))
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getFileMsgContentSample(fileStatus: .rcvTransfer(rcvProgress: 7, rcvTotal: 10)), revealed: Binding.constant(false))
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getFileMsgContentSample(fileStatus: .rcvCancelled), revealed: Binding.constant(false))
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getFileMsgContentSample(fileSize: 1_000_000_000, fileStatus: .rcvInvitation), revealed: Binding.constant(false))
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getFileMsgContentSample(text: "Hello there", fileStatus: .rcvInvitation), revealed: Binding.constant(false))
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getFileMsgContentSample(text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", fileStatus: .rcvInvitation), revealed: Binding.constant(false))
ChatItemView(chat: Chat.sampleData, chatItem: fileChatItemWtFile, revealed: Binding.constant(false))
ChatItemView(chat: Chat.sampleData, chatItem: sentFile)
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getFileMsgContentSample())
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getFileMsgContentSample(fileName: "some_long_file_name_here", fileStatus: .rcvInvitation))
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getFileMsgContentSample(fileStatus: .rcvAccepted))
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getFileMsgContentSample(fileStatus: .rcvTransfer(rcvProgress: 7, rcvTotal: 10)))
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getFileMsgContentSample(fileStatus: .rcvCancelled))
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getFileMsgContentSample(fileSize: 1_000_000_000, fileStatus: .rcvInvitation))
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getFileMsgContentSample(text: "Hello there", fileStatus: .rcvInvitation))
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getFileMsgContentSample(text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", fileStatus: .rcvInvitation))
ChatItemView(chat: Chat.sampleData, chatItem: fileChatItemWtFile)
}
.environment(\.revealed, false)
.previewLayout(.fixed(width: 360, height: 360))
}
}
@@ -45,16 +45,16 @@ struct CIGroupInvitationView: View {
Text(chatIncognito ? "Tap to join incognito" : "Tap to join")
.foregroundColor(inProgress ? theme.colors.secondary : chatIncognito ? .indigo : theme.colors.primary)
.font(.callout)
+ Text(" ")
+ ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, transparent: true, showStatus: false, showEdited: false, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp)
+ Text(verbatim: " ")
+ ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, colorMode: .transparent, showStatus: false, showEdited: false, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp)
)
.overlay(DetermineWidth())
}
} else {
(
groupInvitationText()
+ Text(" ")
+ ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, transparent: true, showStatus: false, showEdited: false, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp)
+ Text(verbatim: " ")
+ ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, colorMode: .transparent, showStatus: false, showEdited: false, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp)
)
.overlay(DetermineWidth())
}
@@ -165,9 +165,9 @@ struct CIImageView: View {
private func fileIcon(_ icon: String, _ size: CGFloat, _ padding: CGFloat) -> some View {
Image(systemName: icon)
.resizable()
.invertedForegroundStyle()
.aspectRatio(contentMode: .fit)
.frame(width: size, height: size)
.foregroundColor(.white)
.padding(padding)
}
@@ -16,7 +16,7 @@ struct CILinkView: View {
var body: some View {
VStack(alignment: .center, spacing: 6) {
if let uiImage = UIImage(base64Encoded: linkPreview.image) {
if let uiImage = imageFromBase64(linkPreview.image) {
Image(uiImage: uiImage)
.resizable()
.scaledToFit()
@@ -23,7 +23,7 @@ struct CIMemberCreatedContactView: View {
.onTapGesture {
dismissAllSheets(animated: true)
DispatchQueue.main.async {
m.chatId = "@\(contactId)"
ItemsModel.shared.loadOpenChat("@\(contactId)")
}
}
} else {
@@ -45,7 +45,7 @@ struct CIMemberCreatedContactView: View {
+ Text(openText)
.fontWeight(.medium)
.foregroundColor(theme.colors.primary)
+ Text(" ")
+ Text(verbatim: " ")
}
r = r + chatItem.timestampText
.fontWeight(.light)
@@ -18,6 +18,7 @@ struct CIMetaView: View {
var paleMetaColor = Color(UIColor.tertiaryLabel)
var showStatus = true
var showEdited = true
var invertedMaterial = false
@AppStorage(DEFAULT_SHOW_SENT_VIA_RPOXY) private var showSentViaProxy = false
@@ -25,18 +26,64 @@ struct CIMetaView: View {
if chatItem.isDeletedContent {
chatItem.timestampText.font(.caption).foregroundColor(metaColor)
} else {
ciMetaText(
chatItem.meta,
chatTTL: chat.chatInfo.timedMessagesTTL,
encrypted: chatItem.encryptedFile,
color: chatItem.meta.itemStatus.sndProgress == .partial
? paleMetaColor
: metaColor,
showStatus: showStatus,
showEdited: showEdited,
showViaProxy: showSentViaProxy,
showTimesamp: showTimestamp
)
ZStack {
ciMetaText(
chatItem.meta,
chatTTL: chat.chatInfo.timedMessagesTTL,
encrypted: chatItem.encryptedFile,
color: metaColor,
paleColor: paleMetaColor,
colorMode: invertedMaterial
? .invertedMaterial
: .normal,
showStatus: showStatus,
showEdited: showEdited,
showViaProxy: showSentViaProxy,
showTimesamp: showTimestamp
).invertedForegroundStyle(enabled: invertedMaterial)
if invertedMaterial {
ciMetaText(
chatItem.meta,
chatTTL: chat.chatInfo.timedMessagesTTL,
encrypted: chatItem.encryptedFile,
colorMode: .normal,
onlyOverrides: true,
showStatus: showStatus,
showEdited: showEdited,
showViaProxy: showSentViaProxy,
showTimesamp: showTimestamp
)
}
}
}
}
}
enum MetaColorMode {
// Renders provided colours
case normal
// Fully transparent meta - used for reserving space
case transparent
// Renders white on dark backgrounds and black on light ones
case invertedMaterial
func resolve(_ c: Color?) -> Color? {
switch self {
case .normal: c
case .transparent: .clear
case .invertedMaterial: nil
}
}
func statusSpacer(_ sent: Bool) -> Text {
switch self {
case .normal, .transparent:
Text(
sent
? Image("checkmark.wide")
: Image(systemName: "circlebadge.fill")
).foregroundColor(.clear)
case .invertedMaterial: textSpace.kerning(13)
}
}
}
@@ -45,47 +92,77 @@ func ciMetaText(
_ meta: CIMeta,
chatTTL: Int?,
encrypted: Bool?,
color: Color = .clear,
color: Color = .clear, // we use this function to reserve space without rendering meta
paleColor: Color? = nil,
primaryColor: Color = .accentColor,
transparent: Bool = false,
colorMode: MetaColorMode = .normal,
onlyOverrides: Bool = false, // only render colors that differ from base
showStatus: Bool = true,
showEdited: Bool = true,
showViaProxy: Bool,
showTimesamp: Bool
) -> Text {
var r = Text("")
var space: Text? = nil
let appendSpace = {
if let sp = space {
r = r + sp
space = nil
}
}
let resolved = colorMode.resolve(color)
if showEdited, meta.itemEdited {
r = r + statusIconText("pencil", color)
r = r + statusIconText("pencil", resolved)
}
if meta.disappearing {
r = r + statusIconText("timer", color).font(.caption2)
r = r + statusIconText("timer", resolved).font(.caption2)
let ttl = meta.itemTimed?.ttl
if ttl != chatTTL {
r = r + Text(shortTimeText(ttl)).foregroundColor(color)
r = r + colored(Text(shortTimeText(ttl)), resolved)
}
r = r + Text(" ")
space = textSpace
}
if showViaProxy, meta.sentViaProxy == true {
r = r + statusIconText("arrow.forward", color.opacity(0.67)).font(.caption2)
appendSpace()
r = r + statusIconText("arrow.forward", resolved?.opacity(0.67)).font(.caption2)
}
if showStatus {
if let (image, statusColor) = meta.itemStatus.statusIcon(color, primaryColor) {
r = r + Text(image).foregroundColor(transparent ? .clear : statusColor) + Text(" ")
appendSpace()
if let (image, statusColor) = meta.itemStatus.statusIcon(color, paleColor ?? color, primaryColor) {
let metaColor = if onlyOverrides && statusColor == color {
Color.clear
} else {
colorMode.resolve(statusColor)
}
r = r + colored(Text(image), metaColor)
} else if !meta.disappearing {
r = r + statusIconText("circlebadge.fill", .clear) + Text(" ")
r = r + colorMode.statusSpacer(meta.itemStatus.sent)
}
space = textSpace
}
if let enc = encrypted {
r = r + statusIconText(enc ? "lock" : "lock.open", color) + Text(" ")
appendSpace()
r = r + statusIconText(enc ? "lock" : "lock.open", resolved)
space = textSpace
}
if showTimesamp {
r = r + meta.timestampText.foregroundColor(color)
appendSpace()
r = r + colored(meta.timestampText, resolved)
}
return r.font(.caption)
}
private func statusIconText(_ icon: String, _ color: Color) -> Text {
Text(Image(systemName: icon)).foregroundColor(color)
private func statusIconText(_ icon: String, _ color: Color?) -> Text {
colored(Text(Image(systemName: icon)), color)
}
// Applying `foregroundColor(nil)` breaks `.invertedForegroundStyle` modifier
private func colored(_ t: Text, _ color: Color?) -> Text {
if let color {
t.foregroundColor(color)
} else {
t
}
}
struct CIMetaView_Previews: PreviewProvider {
@@ -48,7 +48,7 @@ struct CIRcvDecryptionError: View {
if case let .group(groupInfo) = chat.chatInfo,
case let .groupRcv(groupMember) = chatItem.chatDir {
do {
let (member, stats) = try apiGroupMemberInfo(groupInfo.apiId, groupMember.groupMemberId)
let (member, stats) = try apiGroupMemberInfoSync(groupInfo.apiId, groupMember.groupMemberId)
if let s = stats {
m.updateGroupMemberConnectionStats(groupInfo, member, s)
}
@@ -121,12 +121,12 @@ struct CIRcvDecryptionError: View {
Text(Image(systemName: "exclamationmark.arrow.triangle.2.circlepath"))
.foregroundColor(syncSupported ? theme.colors.primary : theme.colors.secondary)
.font(.callout)
+ Text(" ")
+ textSpace
+ Text("Fix connection")
.foregroundColor(syncSupported ? theme.colors.primary : theme.colors.secondary)
.font(.callout)
+ Text(" ")
+ ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, transparent: true, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp)
+ Text(verbatim: " ")
+ ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, colorMode: .transparent, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp)
)
}
.padding(.horizontal, 12)
@@ -144,8 +144,8 @@ struct CIRcvDecryptionError: View {
Text(chatItem.content.text)
.foregroundColor(.red)
.italic()
+ Text(" ")
+ ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, transparent: true, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp)
+ Text(verbatim: " ")
+ ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, colorMode: .transparent, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp)
}
.padding(.horizontal, 12)
CIMetaView(chat: chat, chatItem: chatItem, metaColor: theme.colors.secondary)
@@ -292,30 +292,22 @@ struct CIVideoView: View {
.clipShape(Circle())
}
private func durationProgress() -> some View {
HStack {
Text("\(durationText(videoPlaying ? progress : duration))")
.foregroundColor(.white)
.font(.caption)
.padding(.vertical, 3)
.padding(.horizontal, 6)
.background(Color.black.opacity(0.35))
.cornerRadius(10)
.padding([.top, .leading], 6)
if let file = chatItem.file, !videoPlaying {
Text("\(ByteCountFormatter.string(fromByteCount: file.fileSize, countStyle: .binary))")
.foregroundColor(.white)
.font(.caption)
.padding(.vertical, 3)
.padding(.horizontal, 6)
.background(Color.black.opacity(0.35))
.cornerRadius(10)
.padding(.top, 6)
}
private var fileSizeString: String {
if let file = chatItem.file, !videoPlaying {
" " + ByteCountFormatter.string(fromByteCount: file.fileSize, countStyle: .binary)
} else {
""
}
}
private func durationProgress() -> some View {
Text((durationText(videoPlaying ? progress : duration)) + fileSizeString)
.invertedForegroundStyle()
.font(.caption)
.padding(.vertical, 6)
.padding(.horizontal, 12)
}
private func imageView(_ img: UIImage) -> some View {
let w = img.size.width <= img.size.height ? maxWidth * 0.75 : maxWidth
return ZStack(alignment: .topTrailing) {
@@ -411,9 +403,9 @@ struct CIVideoView: View {
private func fileIcon(_ icon: String, _ size: CGFloat, _ padding: CGFloat) -> some View {
Image(systemName: icon)
.resizable()
.invertedForegroundStyle()
.aspectRatio(contentMode: .fit)
.frame(width: size, height: size)
.foregroundColor(.white)
.padding(smallView ? 0 : padding)
}
@@ -428,10 +420,8 @@ struct CIVideoView: View {
private func progressCircle(_ progress: Int64, _ total: Int64) -> some View {
Circle()
.trim(from: 0, to: Double(progress) / Double(total))
.stroke(
Color(uiColor: .white),
style: StrokeStyle(lineWidth: 2)
)
.stroke(style: StrokeStyle(lineWidth: 2))
.invertedForegroundStyle()
.rotationEffect(.degrees(-90))
.frame(width: 16, height: 16)
.padding([.trailing, .top], smallView ? 0 : 11)
@@ -510,10 +510,10 @@ struct CIVoiceView_Previews: PreviewProvider {
duration: 30,
allowMenu: Binding.constant(true)
)
ChatItemView(chat: Chat.sampleData, chatItem: sentVoiceMessage, revealed: Binding.constant(false), allowMenu: .constant(true))
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getVoiceMsgContentSample(), revealed: Binding.constant(false), allowMenu: .constant(true))
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getVoiceMsgContentSample(fileStatus: .rcvTransfer(rcvProgress: 7, rcvTotal: 10)), revealed: Binding.constant(false), allowMenu: .constant(true))
ChatItemView(chat: Chat.sampleData, chatItem: voiceMessageWtFile, revealed: Binding.constant(false), allowMenu: .constant(true))
ChatItemView(chat: Chat.sampleData, chatItem: sentVoiceMessage, allowMenu: .constant(true))
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getVoiceMsgContentSample(), allowMenu: .constant(true))
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getVoiceMsgContentSample(fileStatus: .rcvTransfer(rcvProgress: 7, rcvTotal: 10)), allowMenu: .constant(true))
ChatItemView(chat: Chat.sampleData, chatItem: voiceMessageWtFile, allowMenu: .constant(true))
}
.previewLayout(.fixed(width: 360, height: 360))
}
@@ -92,12 +92,13 @@ struct FramedCIVoiceView_Previews: PreviewProvider {
file: CIFile.getSample(fileStatus: .sndComplete)
)
Group {
ChatItemView(chat: Chat.sampleData, chatItem: sentVoiceMessage, revealed: Binding.constant(false))
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getVoiceMsgContentSample(text: "Hello there"), revealed: Binding.constant(false))
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getVoiceMsgContentSample(text: "Hello there", fileStatus: .rcvTransfer(rcvProgress: 7, rcvTotal: 10)), revealed: Binding.constant(false))
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getVoiceMsgContentSample(text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."), revealed: Binding.constant(false))
ChatItemView(chat: Chat.sampleData, chatItem: voiceMessageWithQuote, revealed: Binding.constant(false))
ChatItemView(chat: Chat.sampleData, chatItem: sentVoiceMessage)
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getVoiceMsgContentSample(text: "Hello there"))
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getVoiceMsgContentSample(text: "Hello there", fileStatus: .rcvTransfer(rcvProgress: 7, rcvTotal: 10)))
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getVoiceMsgContentSample(text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."))
ChatItemView(chat: Chat.sampleData, chatItem: voiceMessageWithQuote)
}
.environment(\.revealed, false)
.previewLayout(.fixed(width: 360, height: 360))
}
}
File diff suppressed because one or more lines are too long
@@ -12,12 +12,12 @@ import SimpleXChat
struct MarkedDeletedItemView: View {
@EnvironmentObject var m: ChatModel
@EnvironmentObject var theme: AppTheme
@Environment(\.revealed) var revealed: Bool
@ObservedObject var chat: Chat
var chatItem: ChatItem
@Binding var revealed: Bool
var body: some View {
(Text(mergedMarkedDeletedText).italic() + Text(" ") + chatItem.timestampText)
(Text(mergedMarkedDeletedText).italic() + textSpace + chatItem.timestampText)
.font(.caption)
.foregroundColor(theme.colors.secondary)
.padding(.horizontal, 12)
@@ -79,7 +79,10 @@ struct MarkedDeletedItemView: View {
struct MarkedDeletedItemView_Previews: PreviewProvider {
static var previews: some View {
Group {
MarkedDeletedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete), itemDeleted: .deleted(deletedTs: .now)), revealed: Binding.constant(true))
MarkedDeletedItemView(
chat: Chat.sampleData,
chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete), itemDeleted: .deleted(deletedTs: .now))
).environment(\.revealed, true)
}
.previewLayout(.fixed(width: 360, height: 200))
}
@@ -11,7 +11,7 @@ import SimpleXChat
let uiLinkColor = UIColor(red: 0, green: 0.533, blue: 1, alpha: 1)
private let noTyping = Text(" ")
private let noTyping = Text(verbatim: " ")
private let typingIndicators: [Text] = [
(typing(.black) + typing() + typing()),
@@ -85,7 +85,7 @@ struct MsgContentView: View {
}
private func reserveSpaceForMeta(_ mt: CIMeta) -> Text {
(rightToLeft ? Text("\n") : Text(" ")) + ciMetaText(mt, chatTTL: chat.chatInfo.timedMessagesTTL, encrypted: nil, transparent: true, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp)
(rightToLeft ? Text("\n") : Text(verbatim: " ")) + ciMetaText(mt, chatTTL: chat.chatInfo.timedMessagesTTL, encrypted: nil, colorMode: .transparent, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp)
}
}
@@ -104,7 +104,7 @@ func messageText(_ text: String, _ formattedText: [FormattedText]?, _ sender: St
}
if let i = icon {
res = Text(Image(systemName: i)).foregroundColor(secondaryColor) + Text(" ") + res
res = Text(Image(systemName: i)).foregroundColor(secondaryColor) + textSpace + res
}
if let s = sender {
@@ -14,12 +14,11 @@ struct ChatItemForwardingView: View {
@EnvironmentObject var theme: AppTheme
@Environment(\.dismiss) var dismiss
var ci: ChatItem
var chatItems: [ChatItem]
var fromChatInfo: ChatInfo
@Binding var composeState: ComposeState
@State private var searchText: String = ""
@FocusState private var searchFocused
@State private var alert: SomeAlert?
private let chatsToForwardTo = filterChatsToForwardTo(chats: ChatModel.shared.chats)
@@ -46,8 +45,6 @@ struct ChatItemForwardingView: View {
VStack(alignment: .leading) {
if !chatsToForwardTo.isEmpty {
List {
searchFieldView(text: $searchText, focussed: $searchFocused, theme.colors.onBackground, theme.colors.secondary)
.padding(.leading, 2)
let s = searchText.trimmingCharacters(in: .whitespaces).localizedLowercase
let chats = s == "" ? chatsToForwardTo : chatsToForwardTo.filter { foundChat($0, s) }
ForEach(chats) { chat in
@@ -55,6 +52,7 @@ struct ChatItemForwardingView: View {
.disabled(chatModel.deletedChats.contains(chat.chatInfo.id))
}
}
.searchable(text: $searchText, placement: .navigationBarDrawer(displayMode: .always))
.modifier(ThemedBackground(grouped: true))
} else {
ZStack {
@@ -73,11 +71,14 @@ struct ChatItemForwardingView: View {
}
@ViewBuilder private func forwardListChatView(_ chat: Chat) -> some View {
let prohibited = chat.prohibitedByPref(
hasSimplexLink: hasSimplexLink(ci.content.msgContent?.text),
isMediaOrFileAttachment: ci.content.msgContent?.isMediaOrFileAttachment ?? false,
isVoice: ci.content.msgContent?.isVoice ?? false
)
let prohibited = chatItems.map { ci in
chat.prohibitedByPref(
hasSimplexLink: hasSimplexLink(ci.content.msgContent?.text),
isMediaOrFileAttachment: ci.content.msgContent?.isMediaOrFileAttachment ?? false,
isVoice: ci.content.msgContent?.isVoice ?? false
)
}.contains(true)
Button {
if prohibited {
alert = SomeAlert(
@@ -93,10 +94,10 @@ struct ChatItemForwardingView: View {
composeState = ComposeState(
message: composeState.message,
preview: composeState.linkPreview != nil ? composeState.preview : .noPreview,
contextItem: .forwardingItem(chatItem: ci, fromChatInfo: fromChatInfo)
contextItem: .forwardingItems(chatItems: chatItems, fromChatInfo: fromChatInfo)
)
} else {
composeState = ComposeState.init(forwardingItem: ci, fromChatInfo: fromChatInfo)
composeState = ComposeState.init(forwardingItems: chatItems, fromChatInfo: fromChatInfo)
ItemsModel.shared.loadOpenChat(chat.id)
}
}
@@ -123,7 +124,7 @@ struct ChatItemForwardingView: View {
#Preview {
ChatItemForwardingView(
ci: ChatItem.getSample(1, .directSnd, .now, "hello"),
chatItems: [ChatItem.getSample(1, .directSnd, .now, "hello")],
fromChatInfo: .direct(contact: Contact.sampleData),
composeState: Binding.constant(ComposeState(message: "hello"))
).environmentObject(CurrentColors.toAppTheme())
+35 -32
View File
@@ -14,19 +14,28 @@ extension EnvironmentValues {
static let defaultValue: Bool = true
}
struct Revealed: EnvironmentKey {
static let defaultValue: Bool = true
}
var showTimestamp: Bool {
get { self[ShowTimestamp.self] }
set { self[ShowTimestamp.self] = newValue }
}
var revealed: Bool {
get { self[Revealed.self] }
set { self[Revealed.self] = newValue }
}
}
struct ChatItemView: View {
@ObservedObject var chat: Chat
@EnvironmentObject var theme: AppTheme
@Environment(\.showTimestamp) var showTimestamp: Bool
@Environment(\.revealed) var revealed: Bool
var chatItem: ChatItem
var maxWidth: CGFloat = .infinity
@Binding var revealed: Bool
@Binding var allowMenu: Bool
init(
@@ -34,27 +43,25 @@ struct ChatItemView: View {
chatItem: ChatItem,
showMember: Bool = false,
maxWidth: CGFloat = .infinity,
revealed: Binding<Bool>,
allowMenu: Binding<Bool> = .constant(false)
) {
self.chat = chat
self.chatItem = chatItem
self.maxWidth = maxWidth
_revealed = revealed
_allowMenu = allowMenu
}
var body: some View {
let ci = chatItem
if chatItem.meta.itemDeleted != nil && (!revealed || chatItem.isDeletedContent) {
MarkedDeletedItemView(chat: chat, chatItem: chatItem, revealed: $revealed)
MarkedDeletedItemView(chat: chat, chatItem: chatItem)
} else if ci.quotedItem == nil && ci.meta.itemForwarded == nil && ci.meta.itemDeleted == nil && !ci.meta.isLive {
if let mc = ci.content.msgContent, mc.isText && isShortEmoji(ci.content.text) {
EmojiItemView(chat: chat, chatItem: ci)
} else if ci.content.text.isEmpty, case let .voice(_, duration) = ci.content.msgContent {
CIVoiceView(chat: chat, chatItem: ci, recordingFile: ci.file, duration: duration, allowMenu: $allowMenu)
} else if ci.content.msgContent == nil {
ChatItemContentView(chat: chat, chatItem: chatItem, revealed: $revealed, msgContentView: { Text(ci.text) }) // msgContent is unreachable branch in this case
ChatItemContentView(chat: chat, chatItem: chatItem, msgContentView: { Text(ci.text) }) // msgContent is unreachable branch in this case
} else {
framedItemView()
}
@@ -72,7 +79,7 @@ struct ChatItemView: View {
default: nil
}
}
.flatMap { UIImage(base64Encoded: $0) }
.flatMap { imageFromBase64($0) }
let adjustedMaxWidth = {
if let preview, preview.size.width <= preview.size.height {
maxWidth * 0.75
@@ -84,7 +91,6 @@ struct ChatItemView: View {
chat: chat,
chatItem: chatItem,
preview: preview,
revealed: $revealed,
maxWidth: maxWidth,
imgWidth: adjustedMaxWidth,
videoWidth: adjustedMaxWidth,
@@ -96,9 +102,9 @@ struct ChatItemView: View {
struct ChatItemContentView<Content: View>: View {
@EnvironmentObject var chatModel: ChatModel
@EnvironmentObject var theme: AppTheme
@Environment(\.revealed) var revealed: Bool
@ObservedObject var chat: Chat
var chatItem: ChatItem
@Binding var revealed: Bool
var msgContentView: () -> Content
@AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false
@@ -130,7 +136,7 @@ struct ChatItemContentView<Content: View>: View {
case let .rcvChatPreference(feature, allowed, param):
CIFeaturePreferenceView(chat: chat, chatItem: chatItem, feature: feature, allowed: allowed, param: param)
case let .sndChatPreference(feature, _, _):
CIChatFeatureView(chat: chat, chatItem: chatItem, revealed: $revealed, feature: feature, icon: feature.icon, iconColor: theme.colors.secondary)
CIChatFeatureView(chat: chat, chatItem: chatItem, feature: feature, icon: feature.icon, iconColor: theme.colors.secondary)
case let .rcvGroupFeature(feature, preference, _, role): chatFeatureView(feature, preference.enabled(role, for: chat.chatInfo.groupInfo?.membership).iconColor(theme.colors.secondary))
case let .sndGroupFeature(feature, preference, _, role): chatFeatureView(feature, preference.enabled(role, for: chat.chatInfo.groupInfo?.membership).iconColor(theme.colors.secondary))
case let .rcvChatFeatureRejected(feature): chatFeatureView(feature, .red)
@@ -164,7 +170,7 @@ struct ChatItemContentView<Content: View>: View {
private func eventItemViewText(_ secondaryColor: Color) -> Text {
if !revealed, let t = mergedGroupEventText {
return chatEventText(t + Text(" ") + chatItem.timestampText, secondaryColor)
return chatEventText(t + textSpace + chatItem.timestampText, secondaryColor)
} else if let member = chatItem.memberDisplayName {
return Text(member + " ")
.font(.caption)
@@ -177,7 +183,7 @@ struct ChatItemContentView<Content: View>: View {
}
private func chatFeatureView(_ feature: Feature, _ iconColor: Color) -> some View {
CIChatFeatureView(chat: chat, chatItem: chatItem, revealed: $revealed, feature: feature, iconColor: iconColor)
CIChatFeatureView(chat: chat, chatItem: chatItem, feature: feature, iconColor: iconColor)
}
private var mergedGroupEventText: Text? {
@@ -197,7 +203,7 @@ struct ChatItemContentView<Content: View>: View {
} else if ns.count == 0 {
Text("\(count) group events")
} else if count > ns.count {
Text(members) + Text(" ") + Text("and \(count - ns.count) other events")
Text(members) + textSpace + Text("and \(count - ns.count) other events")
} else {
Text(members)
}
@@ -228,7 +234,7 @@ func chatEventText(_ text: Text, _ secondaryColor: Color) -> Text {
}
func chatEventText(_ eventText: LocalizedStringKey, _ ts: Text, _ secondaryColor: Color) -> Text {
chatEventText(Text(eventText) + Text(" ") + ts, secondaryColor)
chatEventText(Text(eventText) + textSpace + ts, secondaryColor)
}
func chatEventText(_ ci: ChatItem, _ secondaryColor: Color) -> Text {
@@ -238,16 +244,17 @@ func chatEventText(_ ci: ChatItem, _ secondaryColor: Color) -> Text {
struct ChatItemView_Previews: PreviewProvider {
static var previews: some View {
Group{
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello"), revealed: Binding.constant(false))
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too"), revealed: Binding.constant(false))
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .directSnd, .now, "🙂"), revealed: Binding.constant(false))
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "🙂🙂🙂🙂🙂"), revealed: Binding.constant(false))
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "🙂🙂🙂🙂🙂🙂"), revealed: Binding.constant(false))
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getDeletedContentSample(), revealed: Binding.constant(false))
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete), itemDeleted: .deleted(deletedTs: .now)), revealed: Binding.constant(false))
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .directSnd, .now, "🙂", .sndSent(sndProgress: .complete), itemLive: true), revealed: Binding.constant(true))
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete), itemLive: true), revealed: Binding.constant(true))
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello"))
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too"))
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .directSnd, .now, "🙂"))
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "🙂🙂🙂🙂🙂"))
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "🙂🙂🙂🙂🙂🙂"))
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getDeletedContentSample())
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete), itemDeleted: .deleted(deletedTs: .now)))
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .directSnd, .now, "🙂", .sndSent(sndProgress: .complete), itemLive: true)).environment(\.revealed, true)
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete), itemLive: true)).environment(\.revealed, true)
}
.environment(\.revealed, false)
.previewLayout(.fixed(width: 360, height: 70))
.environmentObject(Chat.sampleData)
}
@@ -265,8 +272,7 @@ struct ChatItemView_NonMsgContentDeleted_Previews: PreviewProvider {
content: .rcvIntegrityError(msgError: .msgSkipped(fromMsgId: 1, toMsgId: 2)),
quotedItem: nil,
file: nil
),
revealed: Binding.constant(true)
)
)
ChatItemView(
chat: Chat.sampleData,
@@ -276,8 +282,7 @@ struct ChatItemView_NonMsgContentDeleted_Previews: PreviewProvider {
content: .rcvDecryptionError(msgDecryptError: .ratchetHeader, msgCount: 2),
quotedItem: nil,
file: nil
),
revealed: Binding.constant(true)
)
)
ChatItemView(
chat: Chat.sampleData,
@@ -287,8 +292,7 @@ struct ChatItemView_NonMsgContentDeleted_Previews: PreviewProvider {
content: .rcvGroupInvitation(groupInvitation: CIGroupInvitation.getSample(status: .pending), memberRole: .admin),
quotedItem: nil,
file: nil
),
revealed: Binding.constant(true)
)
)
ChatItemView(
chat: Chat.sampleData,
@@ -298,8 +302,7 @@ struct ChatItemView_NonMsgContentDeleted_Previews: PreviewProvider {
content: .rcvGroupEvent(rcvGroupEvent: .memberAdded(groupMemberId: 1, profile: Profile.sampleData)),
quotedItem: nil,
file: nil
),
revealed: Binding.constant(true)
)
)
ChatItemView(
chat: Chat.sampleData,
@@ -309,10 +312,10 @@ struct ChatItemView_NonMsgContentDeleted_Previews: PreviewProvider {
content: ciFeatureContent,
quotedItem: nil,
file: nil
),
revealed: Binding.constant(true)
)
)
}
.environment(\.revealed, true)
.previewLayout(.fixed(width: 360, height: 70))
.environmentObject(Chat.sampleData)
}
+458 -120
View File
@@ -23,7 +23,6 @@ struct ChatView: View {
@Environment(\.scenePhase) var scenePhase
@State @ObservedObject var chat: Chat
@StateObject private var scrollModel = ReverseListScrollModel()
@StateObject private var floatingButtonModel: FloatingButtonModel = .shared
@State private var showChatInfoSheet: Bool = false
@State private var showAddMembersSheet: Bool = false
@State private var composeState = ComposeState()
@@ -43,6 +42,7 @@ struct ChatView: View {
@State private var showGroupLinkSheet: Bool = false
@State private var groupLink: String?
@State private var groupLinkMemberRole: GroupMemberRole = .member
@State private var forwardedChatItems: [ChatItem] = []
@State private var selectedChatItems: Set<Int64>? = nil
@State private var showDeleteSelectedMessages: Bool = false
@State private var allowToDeleteSelectedMessagesForAll: Bool = false
@@ -76,8 +76,7 @@ struct ChatView: View {
VStack(spacing: 0) {
ZStack(alignment: .bottomTrailing) {
chatItemsList()
// TODO: Extract into a separate view, to reduce the scope of `FloatingButtonModel` updates
floatingButtons(unreadBelow: floatingButtonModel.unreadBelow, isNearBottom: floatingButtonModel.isNearBottom)
FloatingButtons(theme: theme, scrollModel: scrollModel, chat: chat)
}
connectingText()
if selectedChatItems == nil {
@@ -100,7 +99,8 @@ struct ChatView: View {
if case let .group(groupInfo) = chat.chatInfo {
showModerateSelectedMessagesAlert(groupInfo)
}
}
},
forwardItems: forwardSelectedMessages
)
}
}
@@ -133,10 +133,39 @@ struct ChatView: View {
.appSheet(item: $selectedMember) { member in
Group {
if case let .group(groupInfo) = chat.chatInfo {
GroupMemberInfoView(groupInfo: groupInfo, groupMember: member, navigation: true)
GroupMemberInfoView(
groupInfo: groupInfo,
chat: chat,
groupMember: member,
navigation: true
)
}
}
}
// it should be presented on top level in order to prevent a bug in SwiftUI on iOS 16 related to .focused() modifier in AddGroupMembersView's search field
.appSheet(isPresented: $showAddMembersSheet) {
Group {
if case let .group(groupInfo) = cInfo {
AddGroupMembersView(chat: chat, groupInfo: groupInfo)
}
}
}
.sheet(isPresented: Binding(
get: { !forwardedChatItems.isEmpty },
set: { isPresented in
if !isPresented {
forwardedChatItems = []
selectedChatItems = nil
}
}
)) {
if #available(iOS 16.0, *) {
ChatItemForwardingView(chatItems: forwardedChatItems, fromChatInfo: chat.chatInfo, composeState: $composeState)
.presentationDetents([.fraction(0.8)])
} else {
ChatItemForwardingView(chatItems: forwardedChatItems, fromChatInfo: chat.chatInfo, composeState: $composeState)
}
}
.onAppear {
selectedChatItems = nil
initChatView()
@@ -202,6 +231,8 @@ struct ChatView: View {
chat: chat,
contact: contact,
localAlias: chat.chatInfo.localAlias,
featuresAllowed: contactUserPrefsToFeaturesAllowed(contact.mergedPreferences),
currentFeaturesAllowed: contactUserPrefsToFeaturesAllowed(contact.mergedPreferences),
onSearch: { focusSearch() }
)
}
@@ -288,9 +319,6 @@ struct ChatView: View {
}
} else {
addMembersButton()
.appSheet(isPresented: $showAddMembersSheet) {
AddGroupMembersView(chat: chat, groupInfo: groupInfo)
}
}
}
Menu {
@@ -341,6 +369,7 @@ struct ChatView: View {
await markChatUnread(chat, unreadChat: false)
}
}
ChatView.FloatingButtonModel.shared.totalUnread = chat.chatStats.unreadCount
}
private func searchToolbar() -> some View {
@@ -411,8 +440,10 @@ struct ChatView: View {
maxWidth: maxWidth,
composeState: $composeState,
selectedMember: $selectedMember,
showChatInfoSheet: $showChatInfoSheet,
revealedChatItem: $revealedChatItem,
selectedChatItems: $selectedChatItems
selectedChatItems: $selectedChatItems,
forwardedChatItems: $forwardedChatItems
)
.id(ci.id) // Required to trigger `onAppear` on iOS15
} loadPage: {
@@ -427,7 +458,7 @@ struct ChatView: View {
.onChange(of: im.itemAdded) { added in
if added {
im.itemAdded = false
if floatingButtonModel.isReallyNearBottom {
if FloatingButtonModel.shared.isReallyNearBottom {
scrollModel.scrollToBottom()
}
}
@@ -453,86 +484,166 @@ struct ChatView: View {
static let shared = FloatingButtonModel()
@Published var unreadBelow: Int = 0
@Published var isNearBottom: Bool = true
var isReallyNearBottom: Bool { scrollOffset.value > 0 && scrollOffset.value < 500 }
let visibleItems = PassthroughSubject<[String], Never>()
let scrollOffset = CurrentValueSubject<Double, Never>(0)
private var bag = Set<AnyCancellable>()
@Published var date: Date?
@Published var isDateVisible: Bool = false
var totalUnread: Int = 0
var isReallyNearBottom: Bool = true
var hideDateWorkItem: DispatchWorkItem?
init() {
visibleItems
.receive(on: DispatchQueue.global(qos: .background))
.map { itemIds in
if let viewId = itemIds.first,
let index = ItemsModel.shared.reversedChatItems.firstIndex(where: { $0.viewId == viewId }) {
ItemsModel.shared.reversedChatItems[..<index].reduce(into: 0) { unread, chatItem in
if chatItem.isRcvNew { unread += 1 }
}
} else { 0 }
func updateOnListChange(_ listState: ListState) {
let im = ItemsModel.shared
let unreadBelow =
if let id = listState.bottomItemId,
let index = im.reversedChatItems.firstIndex(where: { $0.id == id })
{
im.reversedChatItems[..<index].reduce(into: 0) { unread, chatItem in
if chatItem.isRcvNew { unread += 1 }
}
} else {
0
}
let date: Date? =
if let topItemDate = listState.topItemDate {
Calendar.current.startOfDay(for: topItemDate)
} else {
nil
}
.removeDuplicates()
.receive(on: DispatchQueue.main)
.assign(to: \.unreadBelow, on: self)
.store(in: &bag)
scrollOffset
.map { $0 < 800 }
.removeDuplicates()
// Delay the state change until scroll to bottom animation is finished
.delay(for: 0.35, scheduler: DispatchQueue.main)
.assign(to: \.isNearBottom, on: self)
.store(in: &bag)
// set the counters and date indicator
DispatchQueue.main.async { [weak self] in
guard let it = self else { return }
it.setDate(visibility: true)
it.unreadBelow = unreadBelow
it.date = date
it.isReallyNearBottom = listState.scrollOffset > 0 && listState.scrollOffset < 500
}
// set floating button indication mode
let nearBottom = listState.scrollOffset < 800
if nearBottom != self.isNearBottom {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.35) { [weak self] in
self?.isNearBottom = nearBottom
}
}
// hide Date indicator after 1 second of no scrolling
hideDateWorkItem?.cancel()
let workItem = DispatchWorkItem { [weak self] in
guard let it = self else { return }
it.setDate(visibility: false)
it.hideDateWorkItem = nil
}
DispatchQueue.main.async { [weak self] in
self?.hideDateWorkItem = workItem
DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: workItem)
}
}
func resetDate() {
date = nil
isDateVisible = false
}
private func setDate(visibility isVisible: Bool) {
if isVisible {
if !isNearBottom,
!isDateVisible,
let date, !Calendar.current.isDateInToday(date) {
withAnimation { self.isDateVisible = true }
}
} else if isDateVisible {
withAnimation { self.isDateVisible = false }
}
}
}
private func floatingButtons(unreadBelow: Int, isNearBottom: Bool) -> some View {
VStack {
let unreadAbove = chat.chatStats.unreadCount - unreadBelow
if unreadAbove > 0 {
circleButton {
unreadCountText(unreadAbove)
.font(.callout)
.foregroundColor(theme.colors.primary)
private struct FloatingButtons: View {
let theme: AppTheme
let scrollModel: ReverseListScrollModel
let chat: Chat
@ObservedObject var model = FloatingButtonModel.shared
var body: some View {
ZStack(alignment: .top) {
if let date = model.date {
DateSeparator(date: date)
.padding(.vertical, 4).padding(.horizontal, 8)
.background(.thinMaterial)
.clipShape(Capsule())
.opacity(model.isDateVisible ? 1 : 0)
}
.onTapGesture {
scrollModel.scrollToNextPage()
}
.contextMenu {
Button {
Task {
await markChatRead(chat)
VStack {
let unreadAbove = model.totalUnread - model.unreadBelow
if unreadAbove > 0 {
circleButton {
unreadCountText(unreadAbove)
.font(.callout)
.foregroundColor(theme.colors.primary)
}
} label: {
Label("Mark read", systemImage: "checkmark")
.onTapGesture {
scrollModel.scrollToNextPage()
}
.contextMenu {
Button {
Task {
await markChatRead(chat)
}
} label: {
Label("Mark read", systemImage: "checkmark")
}
}
}
Spacer()
if model.unreadBelow > 0 {
circleButton {
unreadCountText(model.unreadBelow)
.font(.callout)
.foregroundColor(theme.colors.primary)
}
.onTapGesture {
scrollModel.scrollToBottom()
}
} else if !model.isNearBottom {
circleButton {
Image(systemName: "chevron.down")
.foregroundColor(theme.colors.primary)
}
.onTapGesture { scrollModel.scrollToBottom() }
}
}
.padding()
.frame(maxWidth: .infinity, alignment: .trailing)
}
Spacer()
if unreadBelow > 0 {
circleButton {
unreadCountText(unreadBelow)
.font(.callout)
.foregroundColor(theme.colors.primary)
}
.onTapGesture {
scrollModel.scrollToBottom()
}
} else if !isNearBottom {
circleButton {
Image(systemName: "chevron.down")
.foregroundColor(theme.colors.primary)
}
.onTapGesture { scrollModel.scrollToBottom() }
.onDisappear(perform: model.resetDate)
}
private func circleButton<Content: View>(_ content: @escaping () -> Content) -> some View {
ZStack {
Circle()
.foregroundColor(Color(uiColor: .tertiarySystemGroupedBackground))
.frame(width: 44, height: 44)
content()
}
}
.padding()
}
private func circleButton<Content: View>(_ content: @escaping () -> Content) -> some View {
ZStack {
Circle()
.foregroundColor(Color(uiColor: .tertiarySystemGroupedBackground))
.frame(width: 44, height: 44)
content()
private struct DateSeparator: View {
let date: Date
var body: some View {
Text(String.localizedStringWithFormat(
NSLocalizedString("%@, %@", comment: "format for date separator in chat"),
date.formatted(.dateTime.weekday(.abbreviated)),
date.formatted(
Calendar.current.isDate(date, equalTo: .now, toGranularity: .year)
? .dateTime.day().month(.abbreviated)
: .dateTime.day().month(.abbreviated).year()
)
))
.font(.callout)
.fontWeight(.medium)
.foregroundStyle(.secondary)
}
}
@@ -626,6 +737,116 @@ struct ChatView: View {
}
}
private func forwardSelectedMessages() {
Task {
do {
if let selectedChatItems {
let (validItems, confirmation) = try await apiPlanForwardChatItems(
type: chat.chatInfo.chatType,
id: chat.chatInfo.apiId,
itemIds: Array(selectedChatItems)
)
if let confirmation {
if validItems.count > 0 {
showAlert(
String.localizedStringWithFormat(
NSLocalizedString("Forward %d message(s)?", comment: "alert title"),
validItems.count
),
message: forwardConfirmationText(confirmation) + "\n" +
NSLocalizedString("Forward messages without files?", comment: "alert message")
) {
switch confirmation {
case let .filesNotAccepted(fileIds):
[forwardAction(validItems), downloadAction(fileIds), cancelAlertAction]
default:
[forwardAction(validItems), cancelAlertAction]
}
}
} else {
showAlert(
NSLocalizedString("Nothing to forward!", comment: "alert title"),
message: forwardConfirmationText(confirmation)
) {
switch confirmation {
case let .filesNotAccepted(fileIds):
[downloadAction(fileIds), cancelAlertAction]
default:
[okAlertAction]
}
}
}
} else {
await openForwardingSheet(validItems)
}
}
} catch {
logger.error("Plan forward chat items failed: \(error.localizedDescription)")
}
}
func forwardConfirmationText(_ fc: ForwardConfirmation) -> String {
switch fc {
case let .filesNotAccepted(fileIds):
String.localizedStringWithFormat(
NSLocalizedString("%d file(s) were not downloaded.", comment: "forward confirmation reason"),
fileIds.count
)
case let .filesInProgress(filesCount):
String.localizedStringWithFormat(
NSLocalizedString("%d file(s) are still being downloaded.", comment: "forward confirmation reason"),
filesCount
)
case let .filesMissing(filesCount):
String.localizedStringWithFormat(
NSLocalizedString("%d file(s) were deleted.", comment: "forward confirmation reason"),
filesCount
)
case let .filesFailed(filesCount):
String.localizedStringWithFormat(
NSLocalizedString("%d file(s) failed to download.", comment: "forward confirmation reason"),
filesCount
)
}
}
func forwardAction(_ items: [Int64]) -> UIAlertAction {
UIAlertAction(
title: NSLocalizedString("Forward messages", comment: "alert action"),
style: .default,
handler: { _ in Task { await openForwardingSheet(items) } }
)
}
func downloadAction(_ fileIds: [Int64]) -> UIAlertAction {
UIAlertAction(
title: NSLocalizedString("Download files", comment: "alert action"),
style: .default,
handler: { _ in
Task {
if let user = ChatModel.shared.currentUser {
await receiveFiles(user: user, fileIds: fileIds)
}
}
}
)
}
func openForwardingSheet(_ items: [Int64]) async {
let im = ItemsModel.shared
var items = Set(items)
var fci = [ChatItem]()
for reversedChatItem in im.reversedChatItems {
if items.contains(reversedChatItem.id) {
items.remove(reversedChatItem.id)
fci.insert(reversedChatItem, at: 0)
}
if items.isEmpty { break }
}
await MainActor.run { forwardedChatItems = fci }
}
}
private func loadChatItems(_ cInfo: ChatInfo) {
Task {
if loadingItems || firstPage { return }
@@ -673,12 +894,14 @@ struct ChatView: View {
private struct ChatItemWithMenu: View {
@EnvironmentObject var m: ChatModel
@EnvironmentObject var theme: AppTheme
@AppStorage(DEFAULT_PROFILE_IMAGE_CORNER_RADIUS) private var profileRadius = defaultProfileImageCorner
@Binding @ObservedObject var chat: Chat
@ObservedObject var dummyModel: ChatItemDummyModel = .shared
let chatItem: ChatItem
let maxWidth: CGFloat
@Binding var composeState: ComposeState
@Binding var selectedMember: GMember?
@Binding var showChatInfoSheet: Bool
@Binding var revealedChatItem: ChatItem?
@State private var deletingItem: ChatItem? = nil
@@ -687,12 +910,13 @@ struct ChatView: View {
@State private var showDeleteMessages = false
@State private var showChatItemInfoSheet: Bool = false
@State private var chatItemInfo: ChatItemInfo?
@State private var showForwardingSheet: Bool = false
@State private var msgWidth: CGFloat = 0
@Binding var selectedChatItems: Set<Int64>?
@Binding var forwardedChatItems: [ChatItem]
@State private var allowMenu: Bool = true
@State private var markedRead = false
var revealed: Bool { chatItem == revealedChatItem }
@@ -704,7 +928,7 @@ struct ChatView: View {
let nextItem = im.reversedChatItems[i - 1]
let largeGap = !nextItem.chatDir.sameDirection(chatItem.chatDir) || nextItem.meta.itemTs.timeIntervalSince(chatItem.meta.itemTs) > 60
return (
timestamp: largeGap || formatTimestampText(chatItem.meta.itemTs) != formatTimestampText(nextItem.meta.itemTs),
timestamp: largeGap || formatTimestampMeta(chatItem.meta.itemTs) != formatTimestampMeta(nextItem.meta.itemTs),
largeGap: largeGap,
date: Calendar.current.isDate(chatItem.meta.itemTs, inSameDayAs: nextItem.meta.itemTs) ? nil : nextItem.meta.itemTs
)
@@ -743,15 +967,7 @@ struct ChatView: View {
VStack(spacing: 0) {
chatItemView(chatItem, range, prevItem, timeSeparation)
if let date = timeSeparation.date {
Text(String.localizedStringWithFormat(
NSLocalizedString("%@, %@", comment: "format for date separator in chat"),
date.formatted(.dateTime.weekday(.abbreviated)),
date.formatted(.dateTime.day().month(.abbreviated))
))
.font(.callout)
.fontWeight(.medium)
.foregroundStyle(.secondary)
.padding(8)
DateSeparator(date: date).padding(8)
}
}
.overlay {
@@ -767,39 +983,43 @@ struct ChatView: View {
}
}
.onAppear {
if markedRead {
return
} else {
markedRead = true
}
if let range {
if let items = unreadItems(range) {
let itemIds = unreadItemIds(range)
if !itemIds.isEmpty {
waitToMarkRead {
for ci in items {
await apiMarkChatItemRead(chat.chatInfo, ci)
}
await apiMarkChatItemsRead(chat.chatInfo, itemIds)
}
}
} else if chatItem.isRcvNew {
waitToMarkRead {
await apiMarkChatItemRead(chat.chatInfo, chatItem)
await apiMarkChatItemsRead(chat.chatInfo, [chatItem.id])
}
}
}
}
private func unreadItems(_ range: ClosedRange<Int>) -> [ChatItem]? {
private func unreadItemIds(_ range: ClosedRange<Int>) -> [ChatItem.ID] {
let im = ItemsModel.shared
let items = range.compactMap { i in
return range.compactMap { i in
if i >= 0 && i < im.reversedChatItems.count {
let ci = im.reversedChatItems[i]
return if ci.isRcvNew { ci } else { nil }
return if ci.isRcvNew { ci.id } else { nil }
} else {
return nil
}
}
return if items.isEmpty { nil } else { items }
}
private func waitToMarkRead(_ op: @Sendable @escaping () async -> Void) {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.6) {
Task {
_ = try? await Task.sleep(nanoseconds: 600_000000)
if m.chatId == chat.chatInfo.id {
Task(operation: op)
await op()
}
}
}
@@ -907,14 +1127,13 @@ struct ChatView: View {
HStack(alignment: .top, spacing: 10) {
MemberProfileImage(member, size: memberImageSize, backgroundColor: theme.colors.background)
.onTapGesture {
if let member = m.getGroupMember(member.groupMemberId) {
selectedMember = member
if let mem = m.getGroupMember(member.groupMemberId) {
selectedMember = mem
} else {
Task {
await m.loadGroupMembers(groupInfo) {
selectedMember = m.getGroupMember(member.groupMemberId)
}
}
let mem = GMember.init(member)
m.groupMembers.append(mem)
m.groupMembersIndexes[member.groupMemberId] = m.groupMembers.count - 1
selectedMember = mem
}
}
chatItemWithMenu(ci, range, maxWidth, itemSeparation)
@@ -973,11 +1192,11 @@ struct ChatView: View {
chat: chat,
chatItem: ci,
maxWidth: maxWidth,
revealed: .constant(revealed),
allowMenu: $allowMenu
)
.environment(\.revealed, revealed)
.environment(\.showTimestamp, itemSeparation.timestamp)
.modifier(ChatItemClipped(ci, tailVisible: itemSeparation.largeGap))
.modifier(ChatItemClipped(ci, tailVisible: itemSeparation.largeGap && (ci.meta.itemDeleted == nil || revealed)))
.contextMenu { menu(ci, range, live: composeState.liveMessage != nil) }
.accessibilityLabel("")
if ci.content.msgContent != nil && (ci.meta.itemDeleted == nil || revealed) && ci.reactions.count > 0 {
@@ -1007,14 +1226,6 @@ struct ChatView: View {
}) {
ChatItemInfoView(ci: ci, chatItemInfo: $chatItemInfo)
}
.sheet(isPresented: $showForwardingSheet) {
if #available(iOS 16.0, *) {
ChatItemForwardingView(ci: ci, fromChatInfo: chat.chatInfo, composeState: $composeState)
.presentationDetents([.fraction(0.8)])
} else {
ChatItemForwardingView(ci: ci, fromChatInfo: chat.chatInfo, composeState: $composeState)
}
}
}
private func showMemberImage(_ member: GroupMember, _ prevItem: ChatItem?) -> Bool {
@@ -1042,12 +1253,27 @@ struct ChatView: View {
}
.padding(.horizontal, 6)
.padding(.vertical, 4)
if chat.chatInfo.featureEnabled(.reactions) && (ci.allowAddReaction || r.userReacted) {
.if(chat.chatInfo.featureEnabled(.reactions) && (ci.allowAddReaction || r.userReacted)) { v in
v.onTapGesture {
setReaction(ci, add: !r.userReacted, reaction: r.reaction)
}
} else {
}
switch chat.chatInfo {
case let .group(groupInfo):
v.contextMenu {
ReactionContextMenu(
groupInfo: groupInfo,
itemId: ci.id,
reactionCount: r,
selectedMember: $selectedMember,
profileRadius: profileRadius
)
}
case let .direct(contact):
v.contextMenu {
contactReactionMenu(contact, r)
}
default:
v
}
}
@@ -1155,7 +1381,7 @@ struct ChatView: View {
var forwardButton: Button<some View> {
Button {
showForwardingSheet = true
forwardedChatItems = [chatItem]
} label: {
Label(
NSLocalizedString("Forward", comment: "chat item action"),
@@ -1550,6 +1776,20 @@ struct ChatView: View {
}
}
}
@ViewBuilder private func contactReactionMenu(_ contact: Contact, _ r: CIReactionCount) -> some View {
if !r.userReacted || r.totalReacted > 1 {
Button { showChatInfoSheet = true } label: {
profileMenuItem(Text(contact.displayName), contact.image, radius: profileRadius)
}
}
if r.userReacted {
Button {} label: {
profileMenuItem(Text("you"), m.currentUser?.profile.image, radius: profileRadius)
}
.disabled(true)
}
}
private struct SelectedChatItem: View {
@EnvironmentObject var theme: AppTheme
@@ -1636,6 +1876,104 @@ private func buildTheme() -> AppTheme {
}
}
struct ReactionContextMenu: View {
@EnvironmentObject var m: ChatModel
let groupInfo: GroupInfo
var itemId: Int64
var reactionCount: CIReactionCount
@Binding var selectedMember: GMember?
var profileRadius: CGFloat
@State private var memberReactions: [MemberReaction] = []
var body: some View {
groupMemberReactionList()
.task {
await loadChatItemReaction()
}
}
@ViewBuilder private func groupMemberReactionList() -> some View {
if memberReactions.isEmpty {
ForEach(Array(repeating: 0, count: reactionCount.totalReacted), id: \.self) { _ in
Text(verbatim: " ")
}
} else {
ForEach(memberReactions, id: \.groupMember.groupMemberId) { mr in
let mem = mr.groupMember
let userMember = mem.groupMemberId == groupInfo.membership.groupMemberId
Button {
if let member = m.getGroupMember(mem.groupMemberId) {
selectedMember = member
} else {
let member = GMember.init(mem)
m.groupMembers.append(member)
m.groupMembersIndexes[member.groupMemberId] = m.groupMembers.count - 1
selectedMember = member
}
} label: {
profileMenuItem(Text(mem.displayName), mem.image, radius: profileRadius)
}
.disabled(userMember)
}
}
}
private func loadChatItemReaction() async {
do {
let memberReactions = try await apiGetReactionMembers(
groupId: groupInfo.groupId,
itemId: itemId,
reaction: reactionCount.reaction
)
await MainActor.run {
self.memberReactions = memberReactions
}
} catch let error {
logger.error("apiGetReactionMembers error: \(responseError(error))")
}
}
}
func profileMenuItem(_ nameText: Text, _ image: String?, radius: CGFloat) -> some View {
HStack {
nameText
if let image, let img = imageFromBase64(image) {
Image(uiImage: maskToCustomShape(img, size: 30, radius: radius))
} else {
Image(systemName: "person.crop.circle")
}
}
}
func maskToCustomShape(_ image: UIImage, size: CGFloat, radius: CGFloat) -> UIImage {
let path = Path { path in
if radius >= 50 {
path.addEllipse(in: CGRect(x: 0, y: 0, width: size, height: size))
} else if radius <= 0 {
path.addRect(CGRect(x: 0, y: 0, width: size, height: size))
} else {
let cornerRadius = size * CGFloat(radius) / 100
path.addRoundedRect(
in: CGRect(x: 0, y: 0, width: size, height: size),
cornerSize: CGSize(width: cornerRadius, height: cornerRadius),
style: .continuous
)
}
}
return UIGraphicsImageRenderer(size: CGSize(width: size, height: size)).image { context in
context.cgContext.addPath(path.cgPath)
context.cgContext.clip()
let scale = size / max(image.size.width, image.size.height)
let imageSize = CGSize(width: image.size.width * scale, height: image.size.height * scale)
let imageOrigin = CGPoint(
x: (size - imageSize.width) / 2,
y: (size - imageSize.height) / 2
)
image.draw(in: CGRect(origin: imageOrigin, size: imageSize))
}
}
struct ToggleNtfsButton: View {
@ObservedObject var chat: Chat
@@ -18,7 +18,7 @@ struct ComposeImageView: View {
var body: some View {
HStack(alignment: .center, spacing: 8) {
let imgs: [UIImage] = images.compactMap { image in
UIImage(base64Encoded: image)
imageFromBase64(image)
}
if imgs.count == 0 {
ProgressView()
@@ -40,7 +40,7 @@ struct ComposeLinkView: View {
private func linkPreviewView(_ linkPreview: LinkPreview) -> some View {
HStack(alignment: .center, spacing: 8) {
if let uiImage = UIImage(base64Encoded: linkPreview.image) {
if let uiImage = imageFromBase64(linkPreview.image) {
Image(uiImage: uiImage)
.resizable()
.aspectRatio(contentMode: .fit)
@@ -23,7 +23,7 @@ enum ComposeContextItem {
case noContextItem
case quotedItem(chatItem: ChatItem)
case editingItem(chatItem: ChatItem)
case forwardingItem(chatItem: ChatItem, fromChatInfo: ChatInfo)
case forwardingItems(chatItems: [ChatItem], fromChatInfo: ChatInfo)
}
enum VoiceMessageRecordingState {
@@ -73,10 +73,10 @@ struct ComposeState {
}
}
init(forwardingItem: ChatItem, fromChatInfo: ChatInfo) {
init(forwardingItems: [ChatItem], fromChatInfo: ChatInfo) {
self.message = ""
self.preview = .noPreview
self.contextItem = .forwardingItem(chatItem: forwardingItem, fromChatInfo: fromChatInfo)
self.contextItem = .forwardingItems(chatItems: forwardingItems, fromChatInfo: fromChatInfo)
self.voiceMessageRecordingState = .noRecording
}
@@ -112,7 +112,7 @@ struct ComposeState {
var forwarding: Bool {
switch contextItem {
case .forwardingItem: return true
case .forwardingItems: return true
default: return false
}
}
@@ -167,6 +167,13 @@ struct ComposeState {
}
}
var manyMediaPreviews: Bool {
switch preview {
case let .mediaPreviews(mediaPreviews): return mediaPreviews.count > 1
default: return false
}
}
var attachmentDisabled: Bool {
if editing || forwarding || liveMessage != nil || inProgress { return true }
switch preview {
@@ -280,8 +287,8 @@ struct ComposeView: View {
// this is a workaround to fire an explicit event in certain cases
@State private var stopPlayback: Bool = false
@AppStorage(DEFAULT_PRIVACY_SAVE_LAST_DRAFT) private var saveLastDraft = true
@AppStorage(DEFAULT_TOOLBAR_MATERIAL) private var toolbarMaterial = ToolbarMaterial.defaultMaterial
@UserDefault(DEFAULT_PRIVACY_SAVE_LAST_DRAFT) private var saveLastDraft = true
@UserDefault(DEFAULT_TOOLBAR_MATERIAL) private var toolbarMaterial = ToolbarMaterial.defaultMaterial
var body: some View {
VStack(spacing: 0) {
@@ -452,7 +459,7 @@ struct ComposeView: View {
Task {
var media: [(String, UploadContent)] = []
for content in selected {
if let img = resizeImageToStrSize(content.uiImage, maxDataSize: 14000) {
if let img = await resizeImageToStrSize(content.uiImage, maxDataSize: 14000) {
media.append((img, content))
await MainActor.run {
composeState = composeState.copy(preview: .mediaPreviews(mediaPreviews: media))
@@ -544,7 +551,7 @@ struct ComposeView: View {
}
private func addMediaContent(_ content: UploadContent) async {
if let img = resizeImageToStrSize(content.uiImage, maxDataSize: 14000) {
if let img = await resizeImageToStrSize(content.uiImage, maxDataSize: 14000) {
var newMedia: [(String, UploadContent?)] = []
if case var .mediaPreviews(media) = composeState.preview {
media.append((img, content))
@@ -687,7 +694,7 @@ struct ComposeView: View {
case let .quotedItem(chatItem: quotedItem):
ContextItemView(
chat: chat,
contextItem: quotedItem,
contextItems: [quotedItem],
contextIcon: "arrowshape.turn.up.left",
cancelContextItem: { composeState = composeState.copy(contextItem: .noContextItem) }
)
@@ -695,18 +702,17 @@ struct ComposeView: View {
case let .editingItem(chatItem: editingItem):
ContextItemView(
chat: chat,
contextItem: editingItem,
contextItems: [editingItem],
contextIcon: "pencil",
cancelContextItem: { clearState() }
)
Divider()
case let .forwardingItem(chatItem: forwardedItem, _):
case let .forwardingItems(chatItems, _):
ContextItemView(
chat: chat,
contextItem: forwardedItem,
contextItems: chatItems,
contextIcon: "arrowshape.turn.up.forward",
cancelContextItem: { composeState = composeState.copy(contextItem: .noContextItem) },
showSender: false
cancelContextItem: { composeState = composeState.copy(contextItem: .noContextItem) }
)
Divider()
}
@@ -730,10 +736,11 @@ struct ComposeView: View {
}
if chat.chatInfo.contact?.nextSendGrpInv ?? false {
await sendMemberContactInvitation()
} else if case let .forwardingItem(ci, fromChatInfo) = composeState.contextItem {
sent = await forwardItem(ci, fromChatInfo, ttl)
} else if case let .forwardingItems(chatItems, fromChatInfo) = composeState.contextItem {
// Composed text is send as a reply to the last forwarded item
sent = await forwardItems(chatItems, fromChatInfo, ttl).last
if !composeState.message.isEmpty {
sent = await send(checkLinkPreview(), quoted: sent?.id, live: false, ttl: ttl)
_ = await send(checkLinkPreview(), quoted: sent?.id, live: false, ttl: ttl)
}
} else if case let .editingItem(ci) = composeState.contextItem {
sent = await updateMessage(ci, live: live)
@@ -750,27 +757,28 @@ struct ComposeView: View {
sent = await send(.text(msgText), quoted: quoted, live: live, ttl: ttl)
case .linkPreview:
sent = await send(checkLinkPreview(), quoted: quoted, live: live, ttl: ttl)
case let .mediaPreviews(mediaPreviews: media):
// TODO batch send: batch media previews
case let .mediaPreviews(media):
let last = media.count - 1
var msgs: [ComposedMessage] = []
if last >= 0 {
for i in 0..<last {
if case (_, .video(_, _, _)) = media[i] {
sent = await sendVideo(media[i], ttl: ttl)
} else {
sent = await sendImage(media[i], ttl: ttl)
if i > 0 {
// Sleep to allow `progressByTimeout` update be rendered
try? await Task.sleep(nanoseconds: 100_000000)
}
if let (fileSource, msgContent) = mediaContent(media[i], text: "") {
msgs.append(ComposedMessage(fileSource: fileSource, msgContent: msgContent))
}
_ = try? await Task.sleep(nanoseconds: 100_000000)
}
if case (_, .video(_, _, _)) = media[last] {
sent = await sendVideo(media[last], text: msgText, quoted: quoted, live: live, ttl: ttl)
} else {
sent = await sendImage(media[last], text: msgText, quoted: quoted, live: live, ttl: ttl)
if let (fileSource, msgContent) = mediaContent(media[last], text: msgText) {
msgs.append(ComposedMessage(fileSource: fileSource, quotedItemId: quoted, msgContent: msgContent))
}
}
if sent == nil {
sent = await send(.text(msgText), quoted: quoted, live: live, ttl: ttl)
if msgs.isEmpty {
msgs = [ComposedMessage(quotedItemId: quoted, msgContent: .text(msgText))]
}
sent = await send(msgs, live: live, ttl: ttl).last
case let .voicePreview(recordingFileName, duration):
stopPlayback.toggle()
let file = voiceCryptoFile(recordingFileName)
@@ -792,6 +800,20 @@ struct ComposeView: View {
}
return sent
func mediaContent(_ media: (String, UploadContent?), text: String) -> (CryptoFile?, MsgContent)? {
let (previewImage, uploadContent) = media
return switch uploadContent {
case let .simpleImage(image):
(saveImage(image), .image(text: text, image: previewImage))
case let .animatedImage(image):
(saveAnimImage(image), .image(text: text, image: previewImage))
case let .video(_, url, duration):
(moveTempFileFromURL(url), .video(text: text, image: previewImage, duration: duration))
case .none:
nil
}
}
func sending() async {
await MainActor.run { composeState.inProgress = true }
}
@@ -855,23 +877,6 @@ struct ComposeView: View {
}
}
func sendImage(_ imageData: (String, UploadContent?), text: String = "", quoted: Int64? = nil, live: Bool = false, ttl: Int?) async -> ChatItem? {
let (image, data) = imageData
if let data = data, let savedFile = saveAnyImage(data) {
return await send(.image(text: text, image: image), quoted: quoted, file: savedFile, live: live, ttl: ttl)
}
return nil
}
func sendVideo(_ imageData: (String, UploadContent?), text: String = "", quoted: Int64? = nil, live: Bool = false, ttl: Int?) async -> ChatItem? {
let (image, data) = imageData
if case let .video(_, url, duration) = data, let savedFile = moveTempFileFromURL(url) {
ChatModel.shared.filesToDelete.remove(url)
return await send(.video(text: text, image: image, duration: duration), quoted: quoted, file: savedFile, live: live, ttl: ttl)
}
return nil
}
func voiceCryptoFile(_ fileName: String) -> CryptoFile? {
if !privacyEncryptLocalFilesGroupDefault.get() {
return CryptoFile.plain(fileName)
@@ -888,17 +893,22 @@ struct ComposeView: View {
}
func send(_ mc: MsgContent, quoted: Int64?, file: CryptoFile? = nil, live: Bool = false, ttl: Int?) async -> ChatItem? {
await send(
[ComposedMessage(fileSource: file, quotedItemId: quoted, msgContent: mc)],
live: live,
ttl: ttl
).first
}
func send(_ msgs: [ComposedMessage], live: Bool, ttl: Int?) async -> [ChatItem] {
if let chatItems = chat.chatInfo.chatType == .local
? await apiCreateChatItems(
noteFolderId: chat.chatInfo.apiId,
composedMessages: [ComposedMessage(fileSource: file, msgContent: mc)]
)
? await apiCreateChatItems(noteFolderId: chat.chatInfo.apiId, composedMessages: msgs)
: await apiSendMessages(
type: chat.chatInfo.chatType,
id: chat.chatInfo.apiId,
live: live,
ttl: ttl,
composedMessages: [ComposedMessage(fileSource: file, quotedItemId: quoted, msgContent: mc)]
composedMessages: msgs
) {
await MainActor.run {
chatModel.removeLiveDummy(animated: false)
@@ -906,33 +916,43 @@ struct ComposeView: View {
chatModel.addChatItem(chat.chatInfo, chatItem)
}
}
// UI only supports sending one item at a time
return chatItems.first
return chatItems
}
if let file = file {
removeFile(file.filePath)
for msg in msgs {
if let file = msg.fileSource {
removeFile(file.filePath)
}
}
return nil
return []
}
func forwardItem(_ forwardedItem: ChatItem, _ fromChatInfo: ChatInfo, _ ttl: Int?) async -> ChatItem? {
func forwardItems(_ forwardedItems: [ChatItem], _ fromChatInfo: ChatInfo, _ ttl: Int?) async -> [ChatItem] {
if let chatItems = await apiForwardChatItems(
toChatType: chat.chatInfo.chatType,
toChatId: chat.chatInfo.apiId,
fromChatType: fromChatInfo.chatType,
fromChatId: fromChatInfo.apiId,
itemIds: [forwardedItem.id],
itemIds: forwardedItems.map { $0.id },
ttl: ttl
) {
await MainActor.run {
for chatItem in chatItems {
chatModel.addChatItem(chat.chatInfo, chatItem)
}
if forwardedItems.count != chatItems.count {
showAlert(
String.localizedStringWithFormat(
NSLocalizedString("%d messages not forwarded", comment: "alert title"),
forwardedItems.count - chatItems.count
),
message: NSLocalizedString("Messages were deleted after you selected them.", comment: "alert message")
)
}
}
// TODO batch send: forward multiple messages
return chatItems.first
return chatItems
} else {
return []
}
return nil
}
func checkLinkPreview() -> MsgContent {
@@ -949,14 +969,6 @@ struct ComposeView: View {
return .text(msgText)
}
}
func saveAnyImage(_ img: UploadContent) -> CryptoFile? {
switch img {
case let .simpleImage(image): return saveImage(image)
case let .animatedImage(image): return saveAnimImage(image)
default: return nil
}
}
}
private func startVoiceMessageRecording() async {
@@ -12,7 +12,7 @@ import SimpleXChat
struct ContextItemView: View {
@EnvironmentObject var theme: AppTheme
@ObservedObject var chat: Chat
let contextItem: ChatItem
let contextItems: [ChatItem]
let contextIcon: String
let cancelContextItem: () -> Void
var showSender: Bool = true
@@ -24,13 +24,22 @@ struct ContextItemView: View {
.aspectRatio(contentMode: .fit)
.frame(width: 16, height: 16)
.foregroundColor(theme.colors.secondary)
if showSender, let sender = contextItem.memberDisplayName {
VStack(alignment: .leading, spacing: 4) {
Text(sender).font(.caption).foregroundColor(theme.colors.secondary)
msgContentView(lines: 2)
}
if let singleItem = contextItems.first, contextItems.count == 1 {
if showSender, let sender = singleItem.memberDisplayName {
VStack(alignment: .leading, spacing: 4) {
Text(sender).font(.caption).foregroundColor(theme.colors.secondary)
msgContentView(lines: 2, contextItem: singleItem)
}
} else {
msgContentView(lines: 3, contextItem: singleItem)
}
} else {
msgContentView(lines: 3)
Text(
chat.chatInfo.chatType == .local
? "Saving \(contextItems.count) messages"
: "Forwarding \(contextItems.count) messages"
)
.italic()
}
Spacer()
Button {
@@ -45,29 +54,38 @@ struct ContextItemView: View {
.padding(12)
.frame(minHeight: 54)
.frame(maxWidth: .infinity)
.background(chatItemFrameColor(contextItem, theme))
.background(background)
}
private func msgContentView(lines: Int) -> some View {
contextMsgPreview()
private var background: Color {
contextItems.first
.map { chatItemFrameColor($0, theme) }
?? Color(uiColor: .tertiarySystemBackground)
}
private func msgContentView(lines: Int, contextItem: ChatItem) -> some View {
contextMsgPreview(contextItem)
.multilineTextAlignment(isRightToLeft(contextItem.text) ? .trailing : .leading)
.lineLimit(lines)
}
private func contextMsgPreview() -> Text {
private func contextMsgPreview(_ contextItem: ChatItem) -> Text {
return attachment() + messageText(contextItem.text, contextItem.formattedText, nil, preview: true, showSecrets: false, secondaryColor: theme.colors.secondary)
func attachment() -> Text {
let isFileLoaded = if let fileSource = getLoadedFileSource(contextItem.file) {
FileManager.default.fileExists(atPath: getAppFilePath(fileSource.filePath).path)
} else { false }
switch contextItem.content.msgContent {
case .file: return image("doc.fill")
case .file: return isFileLoaded ? image("doc.fill") : Text("")
case .image: return image("photo")
case .voice: return image("play.fill")
case .voice: return isFileLoaded ? image("play.fill") : Text("")
default: return Text("")
}
}
func image(_ s: String) -> Text {
Text(Image(systemName: s)).foregroundColor(Color(uiColor: .tertiaryLabel)) + Text(" ")
Text(Image(systemName: s)).foregroundColor(Color(uiColor: .tertiaryLabel)) + textSpace
}
}
}
@@ -75,6 +93,6 @@ struct ContextItemView: View {
struct ContextItemView_Previews: PreviewProvider {
static var previews: some View {
let contextItem: ChatItem = ChatItem.getSample(1, .directSnd, .now, "hello")
return ContextItemView(chat: Chat.sampleData, contextItem: contextItem, contextIcon: "pencil.circle", cancelContextItem: {})
return ContextItemView(chat: Chat.sampleData, contextItems: [contextItem], contextIcon: "pencil.circle", cancelContextItem: {})
}
}
@@ -40,7 +40,7 @@ struct SendMessageView: View {
@State private var showCustomTimePicker = false
@State private var selectedDisappearingMessageTime: Int? = customDisappearingMessageTimeDefault.get()
@State private var progressByTimeout = false
@AppStorage(DEFAULT_LIVE_MESSAGE_ALERT_SHOWN) private var liveMessageAlertShown = false
@UserDefault(DEFAULT_LIVE_MESSAGE_ALERT_SHOWN) private var liveMessageAlertShown = false
var body: some View {
ZStack {
@@ -227,6 +227,7 @@ struct SendMessageView: View {
!composeState.editing {
if case .noContextItem = composeState.contextItem,
!composeState.voicePreview,
!composeState.manyMediaPreviews,
let send = sendLiveMessage,
let update = updateLiveMessage {
Button {
@@ -14,9 +14,10 @@ struct ContactPreferencesView: View {
@EnvironmentObject var chatModel: ChatModel
@EnvironmentObject var theme: AppTheme
@Binding var contact: Contact
@State var featuresAllowed: ContactFeaturesAllowed
@State var currentFeaturesAllowed: ContactFeaturesAllowed
@Binding var featuresAllowed: ContactFeaturesAllowed
@Binding var currentFeaturesAllowed: ContactFeaturesAllowed
@State private var showSaveDialogue = false
let savePreferences: () -> Void
var body: some View {
let user: User = chatModel.currentUser!
@@ -48,7 +49,10 @@ struct ContactPreferencesView: View {
savePreferences()
dismiss()
}
Button("Exit without saving") { dismiss() }
Button("Exit without saving") {
featuresAllowed = currentFeaturesAllowed
dismiss()
}
}
}
@@ -118,31 +122,15 @@ struct ContactPreferencesView: View {
private func featureFooter(_ feature: ChatFeature, _ enabled: FeatureEnabled) -> some View {
Text(feature.enabledDescription(enabled))
}
private func savePreferences() {
Task {
do {
let prefs = contactFeaturesAllowedToPrefs(featuresAllowed)
if let toContact = try await apiSetContactPrefs(contactId: contact.contactId, preferences: prefs) {
await MainActor.run {
contact = toContact
chatModel.updateContact(toContact)
currentFeaturesAllowed = featuresAllowed
}
}
} catch {
logger.error("ContactPreferencesView apiSetContactPrefs error: \(responseError(error))")
}
}
}
}
struct ContactPreferencesView_Previews: PreviewProvider {
static var previews: some View {
ContactPreferencesView(
contact: Binding.constant(Contact.sampleData),
featuresAllowed: ContactFeaturesAllowed.sampleData,
currentFeaturesAllowed: ContactFeaturesAllowed.sampleData
featuresAllowed: Binding.constant(ContactFeaturesAllowed.sampleData),
currentFeaturesAllowed: Binding.constant(ContactFeaturesAllowed.sampleData),
savePreferences: {}
)
}
}
@@ -78,7 +78,12 @@ struct AddGroupMembersViewCommon: View {
let count = selectedContacts.count
Section {
if creatingGroup {
groupPreferencesButton($groupInfo, true)
GroupPreferencesButton(
groupInfo: $groupInfo,
preferences: groupInfo.fullGroupPreferences,
currentPreferences: groupInfo.fullGroupPreferences,
creatingGroup: true
)
}
rolePicker()
inviteMembersButton()
@@ -105,8 +110,10 @@ struct AddGroupMembersViewCommon: View {
.padding(.leading, 2)
let s = searchText.trimmingCharacters(in: .whitespaces).localizedLowercase
let members = s == "" ? membersToAdd : membersToAdd.filter { $0.chatViewName.localizedLowercase.contains(s) }
ForEach(members) { contact in
contactCheckView(contact)
ForEach(members + [dummyContact]) { contact in
if contact.contactId != dummyContact.contactId {
contactCheckView(contact)
}
}
}
}
@@ -130,12 +137,21 @@ struct AddGroupMembersViewCommon: View {
.modifier(ThemedBackground(grouped: true))
}
private func inviteMembersButton() -> some View {
// Resolves keyboard losing focus bug in iOS16 and iOS17,
// when there are no items inside `ForEach(memebers)` loop
private let dummyContact: Contact = {
var dummy = Contact.sampleData
dummy.contactId = -1
return dummy
}()
@ViewBuilder private func inviteMembersButton() -> some View {
let label: LocalizedStringKey = groupInfo.businessChat == nil ? "Invite to group" : "Invite to chat"
Button {
inviteMembers()
} label: {
HStack {
Text("Invite to group")
Text(label)
Image(systemName: "checkmark")
}
}
@@ -221,6 +237,7 @@ func searchFieldView(text: Binding<String>, focussed: FocusState<Bool>.Binding,
.focused(focussed)
.foregroundColor(onBackgroundColor)
.frame(maxWidth: .infinity)
.autocorrectionDisabled(true)
Image(systemName: "xmark.circle.fill")
.resizable()
.scaledToFit()
@@ -81,13 +81,13 @@ struct GroupChatInfoView: View {
.listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
Section {
if groupInfo.canEdit {
if groupInfo.isOwner && groupInfo.businessChat == nil {
editGroupButton()
}
if groupInfo.groupProfile.description != nil || groupInfo.canEdit {
if groupInfo.groupProfile.description != nil || (groupInfo.isOwner && groupInfo.businessChat == nil) {
addOrEditWelcomeMessage()
}
groupPreferencesButton($groupInfo)
GroupPreferencesButton(groupInfo: $groupInfo, preferences: groupInfo.fullGroupPreferences, currentPreferences: groupInfo.fullGroupPreferences)
if members.filter({ $0.wrapped.memberCurrent }).count <= SMALL_GROUPS_RCPS_MEM_LIMIT {
sendReceiptsOption()
} else {
@@ -101,13 +101,20 @@ struct GroupChatInfoView: View {
} header: {
Text("")
} footer: {
Text("Only group owners can change group preferences.")
let label: LocalizedStringKey = (
groupInfo.businessChat == nil
? "Only group owners can change group preferences."
: "Only chat owners can change preferences."
)
Text(label)
.foregroundColor(theme.colors.secondary)
}
Section(header: Text("\(members.count + 1) members").foregroundColor(theme.colors.secondary)) {
if groupInfo.canAddMembers {
groupLinkButton()
if groupInfo.businessChat == nil {
groupLinkButton()
}
if (chat.chatInfo.incognito) {
Label("Invite members", systemImage: "plus")
.foregroundColor(Color(uiColor: .tertiaryLabel))
@@ -276,10 +283,15 @@ struct GroupChatInfoView: View {
}
private func addMembersButton() -> some View {
NavigationLink {
let label: LocalizedStringKey = switch groupInfo.businessChat?.chatType {
case .customer: "Add team members"
case .business: "Add friends"
case .none: "Invite members"
}
return NavigationLink {
addMembersDestinationView()
} label: {
Label("Invite members", systemImage: "plus")
Label(label, systemImage: "plus")
}
}
@@ -418,7 +430,7 @@ struct GroupChatInfoView: View {
}
private var memberVerifiedShield: Text {
(Text(Image(systemName: "checkmark.shield")) + Text(" "))
(Text(Image(systemName: "checkmark.shield")) + textSpace)
.font(.caption)
.baselineOffset(2)
.kerning(-2)
@@ -427,7 +439,7 @@ struct GroupChatInfoView: View {
}
private func memberInfoView(_ groupMember: GMember) -> some View {
GroupMemberInfoView(groupInfo: groupInfo, groupMember: groupMember)
GroupMemberInfoView(groupInfo: groupInfo, chat: chat, groupMember: groupMember)
.navigationBarHidden(false)
}
@@ -487,11 +499,12 @@ struct GroupChatInfoView: View {
}
}
private func deleteGroupButton() -> some View {
@ViewBuilder private func deleteGroupButton() -> some View {
let label: LocalizedStringKey = groupInfo.businessChat == nil ? "Delete group" : "Delete chat"
Button(role: .destructive) {
alert = .deleteGroupAlert
} label: {
Label("Delete group", systemImage: "trash")
Label(label, systemImage: "trash")
.foregroundColor(Color.red)
}
}
@@ -505,20 +518,22 @@ struct GroupChatInfoView: View {
}
}
private func leaveGroupButton() -> some View {
@ViewBuilder private func leaveGroupButton() -> some View {
let label: LocalizedStringKey = groupInfo.businessChat == nil ? "Leave group" : "Leave chat"
Button(role: .destructive) {
alert = .leaveGroupAlert
} label: {
Label("Leave group", systemImage: "rectangle.portrait.and.arrow.right")
Label(label, systemImage: "rectangle.portrait.and.arrow.right")
.foregroundColor(Color.red)
}
}
// TODO reuse this and clearChatAlert with ChatInfoView
private func deleteGroupAlert() -> Alert {
let label: LocalizedStringKey = groupInfo.businessChat == nil ? "Delete group?" : "Delete chat?"
return Alert(
title: Text("Delete group?"),
message: deleteGroupAlertMessage(),
title: Text(label),
message: deleteGroupAlertMessage(groupInfo),
primaryButton: .destructive(Text("Delete")) {
Task {
do {
@@ -537,10 +552,6 @@ struct GroupChatInfoView: View {
)
}
private func deleteGroupAlertMessage() -> Text {
groupInfo.membership.memberCurrent ? Text("Group will be deleted for all members - this cannot be undone!") : Text("Group will be deleted for you - this cannot be undone!")
}
private func clearChatAlert() -> Alert {
Alert(
title: Text("Clear conversation?"),
@@ -556,9 +567,15 @@ struct GroupChatInfoView: View {
}
private func leaveGroupAlert() -> Alert {
Alert(
title: Text("Leave group?"),
message: Text("You will stop receiving messages from this group. Chat history will be preserved."),
let titleLabel: LocalizedStringKey = groupInfo.businessChat == nil ? "Leave group?" : "Leave chat?"
let messageLabel: LocalizedStringKey = (
groupInfo.businessChat == nil
? "You will stop receiving messages from this group. Chat history will be preserved."
: "You will stop receiving messages from this chat. Chat history will be preserved."
)
return Alert(
title: Text(titleLabel),
message: Text(messageLabel),
primaryButton: .destructive(Text("Leave")) {
Task {
await leaveGroup(chat.chatInfo.apiId)
@@ -602,9 +619,14 @@ struct GroupChatInfoView: View {
}
private func removeMemberAlert(_ mem: GroupMember) -> Alert {
Alert(
let messageLabel: LocalizedStringKey = (
groupInfo.businessChat == nil
? "Member will be removed from group - this cannot be undone!"
: "Member will be removed from chat - this cannot be undone!"
)
return Alert(
title: Text("Remove member?"),
message: Text("Member will be removed from group - this cannot be undone!"),
message: Text(messageLabel),
primaryButton: .destructive(Text("Remove")) {
Task {
do {
@@ -624,26 +646,80 @@ struct GroupChatInfoView: View {
}
}
func groupPreferencesButton(_ groupInfo: Binding<GroupInfo>, _ creatingGroup: Bool = false) -> some View {
NavigationLink {
GroupPreferencesView(
groupInfo: groupInfo,
preferences: groupInfo.wrappedValue.fullGroupPreferences,
currentPreferences: groupInfo.wrappedValue.fullGroupPreferences,
creatingGroup: creatingGroup
)
.navigationBarTitle("Group preferences")
.modifier(ThemedBackground(grouped: true))
.navigationBarTitleDisplayMode(.large)
} label: {
if creatingGroup {
Text("Set group preferences")
} else {
Label("Group preferences", systemImage: "switch.2")
func deleteGroupAlertMessage(_ groupInfo: GroupInfo) -> Text {
groupInfo.businessChat == nil ? (
groupInfo.membership.memberCurrent ? Text("Group will be deleted for all members - this cannot be undone!") : Text("Group will be deleted for you - this cannot be undone!")
) : (
groupInfo.membership.memberCurrent ? Text("Chat will be deleted for all members - this cannot be undone!") : Text("Chat will be deleted for you - this cannot be undone!")
)
}
struct GroupPreferencesButton: View {
@Binding var groupInfo: GroupInfo
@State var preferences: FullGroupPreferences
@State var currentPreferences: FullGroupPreferences
var creatingGroup: Bool = false
private var label: LocalizedStringKey {
groupInfo.businessChat == nil ? "Group preferences" : "Chat preferences"
}
var body: some View {
NavigationLink {
GroupPreferencesView(
groupInfo: $groupInfo,
preferences: $preferences,
currentPreferences: currentPreferences,
creatingGroup: creatingGroup,
savePreferences: savePreferences
)
.navigationBarTitle(label)
.modifier(ThemedBackground(grouped: true))
.navigationBarTitleDisplayMode(.large)
.onDisappear {
let saveText = NSLocalizedString(
creatingGroup ? "Save" : "Save and notify group members",
comment: "alert button"
)
if groupInfo.fullGroupPreferences != preferences {
showAlert(
title: NSLocalizedString("Save preferences?", comment: "alert title"),
buttonTitle: saveText,
buttonAction: { savePreferences() },
cancelButton: true
)
}
}
} label: {
if creatingGroup {
Text("Set group preferences")
} else {
Label(label, systemImage: "switch.2")
}
}
}
private func savePreferences() {
Task {
do {
var gp = groupInfo.groupProfile
gp.groupPreferences = toGroupPreferences(preferences)
let gInfo = try await apiUpdateGroup(groupInfo.groupId, gp)
await MainActor.run {
groupInfo = gInfo
ChatModel.shared.updateGroup(gInfo)
currentPreferences = preferences
}
} catch {
logger.error("GroupPreferencesView apiUpdateGroup error: \(responseError(error))")
}
}
}
}
func cantInviteIncognitoAlert() -> Alert {
Alert(
title: Text("Can't invite contacts!"),
@@ -14,10 +14,15 @@ struct GroupMemberInfoView: View {
@EnvironmentObject var theme: AppTheme
@Environment(\.dismiss) var dismiss: DismissAction
@State var groupInfo: GroupInfo
@ObservedObject var chat: Chat
@ObservedObject var groupMember: GMember
var navigation: Bool = false
@State private var connectionStats: ConnectionStats? = nil
@State private var connectionCode: String? = nil
@State private var connectionLoaded: Bool = false
@State private var knownContactChat: Chat? = nil
@State private var knownContact: Contact? = nil
@State private var knownContactConnectionStats: ConnectionStats? = nil
@State private var newRole: GroupMemberRole = .member
@State private var alert: GroupMemberInfoViewAlert?
@State private var sheet: PlanAndConnectActionSheet?
@@ -94,128 +99,149 @@ struct GroupMemberInfoView: View {
.listRowSeparator(.hidden)
.listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
if member.memberActive {
Section {
if let code = connectionCode { verifyCodeButton(code) }
if let connStats = connectionStats,
connStats.ratchetSyncAllowed {
synchronizeConnectionButton()
}
// } else if developerTools {
// synchronizeConnectionButtonForce()
// }
}
}
if connectionLoaded {
if let contactLink = member.contactLink {
Section {
SimpleXLinkQRCode(uri: contactLink)
Button {
showShareSheet(items: [simplexChatLink(contactLink)])
} label: {
Label("Share address", systemImage: "square.and.arrow.up")
if member.memberActive {
Section {
if let code = connectionCode { verifyCodeButton(code) }
if let connStats = connectionStats,
connStats.ratchetSyncAllowed {
synchronizeConnectionButton()
}
// } else if developerTools {
// synchronizeConnectionButtonForce()
// }
}
if let contactId = member.memberContactId {
if knownDirectChat(contactId) == nil && !groupInfo.fullGroupPreferences.directMessages.on(for: groupInfo.membership) {
}
if let contactLink = member.contactLink {
Section {
SimpleXLinkQRCode(uri: contactLink)
Button {
showShareSheet(items: [simplexChatLink(contactLink)])
} label: {
Label("Share address", systemImage: "square.and.arrow.up")
}
if member.memberContactId != nil {
if knownContactChat == nil && !groupInfo.fullGroupPreferences.directMessages.on(for: groupInfo.membership) {
connectViaAddressButton(contactLink)
}
} else {
connectViaAddressButton(contactLink)
}
} else {
connectViaAddressButton(contactLink)
} header: {
Text("Address")
.foregroundColor(theme.colors.secondary)
} footer: {
Text("You can share this address with your contacts to let them connect with **\(member.displayName)**.")
.foregroundColor(theme.colors.secondary)
}
} header: {
Text("Address")
.foregroundColor(theme.colors.secondary)
} footer: {
Text("You can share this address with your contacts to let them connect with **\(member.displayName)**.")
.foregroundColor(theme.colors.secondary)
}
}
Section(header: Text("Member").foregroundColor(theme.colors.secondary)) {
infoRow("Group", groupInfo.displayName)
Section(header: Text("Member").foregroundColor(theme.colors.secondary)) {
let label: LocalizedStringKey = groupInfo.businessChat == nil ? "Group" : "Chat"
infoRow(label, groupInfo.displayName)
if let roles = member.canChangeRoleTo(groupInfo: groupInfo) {
Picker("Change role", selection: $newRole) {
ForEach(roles) { role in
Text(role.text)
if let roles = member.canChangeRoleTo(groupInfo: groupInfo) {
Picker("Change role", selection: $newRole) {
ForEach(roles) { role in
Text(role.text)
}
}
.frame(height: 36)
} else {
infoRow("Role", member.memberRole.text)
}
.frame(height: 36)
} else {
infoRow("Role", member.memberRole.text)
}
}
if let connStats = connectionStats {
Section(header: Text("Servers").foregroundColor(theme.colors.secondary)) {
// TODO network connection status
Button("Change receiving address") {
alert = .switchAddressAlert
}
.disabled(
connStats.rcvQueuesInfo.contains { $0.rcvSwitchStatus != nil }
|| connStats.ratchetSyncSendProhibited
)
if connStats.rcvQueuesInfo.contains(where: { $0.rcvSwitchStatus != nil }) {
Button("Abort changing address") {
alert = .abortSwitchAddressAlert
if let connStats = connectionStats {
Section(header: Text("Servers").foregroundColor(theme.colors.secondary)) {
// TODO network connection status
Button("Change receiving address") {
alert = .switchAddressAlert
}
.disabled(
connStats.rcvQueuesInfo.contains { $0.rcvSwitchStatus != nil && !$0.canAbortSwitch }
connStats.rcvQueuesInfo.contains { $0.rcvSwitchStatus != nil }
|| connStats.ratchetSyncSendProhibited
)
if connStats.rcvQueuesInfo.contains(where: { $0.rcvSwitchStatus != nil }) {
Button("Abort changing address") {
alert = .abortSwitchAddressAlert
}
.disabled(
connStats.rcvQueuesInfo.contains { $0.rcvSwitchStatus != nil && !$0.canAbortSwitch }
|| connStats.ratchetSyncSendProhibited
)
}
smpServers("Receiving via", connStats.rcvQueuesInfo.map { $0.rcvServer }, theme.colors.secondary)
smpServers("Sending via", connStats.sndQueuesInfo.map { $0.sndServer }, theme.colors.secondary)
}
smpServers("Receiving via", connStats.rcvQueuesInfo.map { $0.rcvServer }, theme.colors.secondary)
smpServers("Sending via", connStats.sndQueuesInfo.map { $0.sndServer }, theme.colors.secondary)
}
}
if groupInfo.membership.memberRole >= .admin {
adminDestructiveSection(member)
} else {
nonAdminBlockSection(member)
}
if groupInfo.membership.memberRole >= .admin {
adminDestructiveSection(member)
} else {
nonAdminBlockSection(member)
}
if developerTools {
Section(header: Text("For console").foregroundColor(theme.colors.secondary)) {
infoRow("Local name", member.localDisplayName)
infoRow("Database ID", "\(member.groupMemberId)")
if let conn = member.activeConn {
let connLevelDesc = conn.connLevel == 0 ? NSLocalizedString("direct", comment: "connection level description") : String.localizedStringWithFormat(NSLocalizedString("indirect (%d)", comment: "connection level description"), conn.connLevel)
infoRow("Connection", connLevelDesc)
}
Button ("Debug delivery") {
Task {
do {
let info = queueInfoText(try await apiGroupMemberQueueInfo(groupInfo.apiId, member.groupMemberId))
await MainActor.run { alert = .queueInfo(info: info) }
} catch let e {
logger.error("apiContactQueueInfo error: \(responseError(e))")
let a = getErrorAlert(e, "Error")
await MainActor.run { alert = .error(title: a.title, error: a.message) }
if developerTools {
Section(header: Text("For console").foregroundColor(theme.colors.secondary)) {
infoRow("Local name", member.localDisplayName)
infoRow("Database ID", "\(member.groupMemberId)")
if let conn = member.activeConn {
let connLevelDesc = conn.connLevel == 0 ? NSLocalizedString("direct", comment: "connection level description") : String.localizedStringWithFormat(NSLocalizedString("indirect (%d)", comment: "connection level description"), conn.connLevel)
infoRow("Connection", connLevelDesc)
}
Button ("Debug delivery") {
Task {
do {
let info = queueInfoText(try await apiGroupMemberQueueInfo(groupInfo.apiId, member.groupMemberId))
await MainActor.run { alert = .queueInfo(info: info) }
} catch let e {
logger.error("apiContactQueueInfo error: \(responseError(e))")
let a = getErrorAlert(e, "Error")
await MainActor.run { alert = .error(title: a.title, error: a.message) }
}
}
}
}
}
}
}
.navigationBarHidden(true)
.onAppear {
.task {
if #unavailable(iOS 16) {
// this condition prevents re-setting picker
if !justOpened { return }
}
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)
newRole = member.memberRole
do {
let (_, stats) = try await apiGroupMemberInfo(groupInfo.apiId, member.groupMemberId)
let (mem, code) = member.memberActive ? try await apiGetGroupMemberCode(groupInfo.apiId, member.groupMemberId) : (member, nil)
await MainActor.run {
_ = chatModel.upsertGroupMember(groupInfo, mem)
connectionStats = stats
connectionCode = code
connectionLoaded = true
}
} catch let error {
await MainActor.run {
connectionLoaded = true
}
logger.error("apiGroupMemberInfo or apiGetGroupMemberCode error: \(responseError(error))")
}
if let contactId = member.memberContactId, let (contactChat, contact) = knownDirectChat(contactId) {
knownContactChat = contactChat
knownContact = contact
do {
let (stats, _) = try await apiContactInfo(contactChat.chatInfo.apiId)
await MainActor.run {
knownContactConnectionStats = stats
}
} catch let error {
logger.error("apiGroupMemberInfo or apiGetGroupMemberCode error: \(responseError(error))")
logger.error("apiContactInfo error: \(responseError(error))")
}
}
}
@@ -251,6 +277,11 @@ struct GroupMemberInfoView: View {
ProgressView().scaleEffect(2)
}
}
.onChange(of: chat.chatInfo) { c in
if case let .group(gI) = chat.chatInfo {
groupInfo = gI
}
}
.modifier(ThemedBackground(grouped: true))
}
@@ -258,10 +289,10 @@ struct GroupMemberInfoView: View {
GeometryReader { g in
let buttonWidth = g.size.width / 4
HStack(alignment: .center, spacing: 8) {
if let contactId = member.memberContactId, let (chat, contact) = knownDirectChat(contactId) {
if let chat = knownContactChat, let contact = knownContact {
knownDirectChatButton(chat, width: buttonWidth)
AudioCallButton(chat: chat, contact: contact, width: buttonWidth) { alert = .someAlert(alert: $0) }
VideoButton(chat: chat, contact: contact, width: buttonWidth) { alert = .someAlert(alert: $0) }
AudioCallButton(chat: chat, contact: contact, connectionStats: $knownContactConnectionStats, width: buttonWidth) { alert = .someAlert(alert: $0) }
VideoButton(chat: chat, contact: contact, connectionStats: $knownContactConnectionStats, width: buttonWidth) { alert = .someAlert(alert: $0) }
} else if groupInfo.fullGroupPreferences.directMessages.on(for: groupInfo.membership) {
if let contactId = member.memberContactId {
newDirectChatButton(contactId, width: buttonWidth)
@@ -296,10 +327,15 @@ struct GroupMemberInfoView: View {
}
func showDirectMessagesProhibitedAlert(_ title: LocalizedStringKey) {
let messageLabel: LocalizedStringKey = (
groupInfo.businessChat == nil
? "Direct messages between members are prohibited."
: "Direct messages between members are prohibited in this chat."
)
alert = .someAlert(alert: SomeAlert(
alert: mkAlert(
title: title,
message: "Direct messages between members are prohibited in this group."
message: messageLabel
),
id: "can't message member, direct messages prohibited"
))
@@ -345,25 +381,49 @@ struct GroupMemberInfoView: View {
func createMemberContactButton(width: CGFloat) -> some View {
InfoViewButton(image: "message.fill", title: "message", width: width) {
progressIndicator = true
Task {
do {
let memberContact = try await apiCreateMemberContact(groupInfo.apiId, groupMember.groupMemberId)
await MainActor.run {
progressIndicator = false
chatModel.addChat(Chat(chatInfo: .direct(contact: memberContact)))
ItemsModel.shared.loadOpenChat(memberContact.id) {
dismissAllSheets(animated: true)
if let connStats = connectionStats {
if connStats.ratchetSyncState == .ok {
progressIndicator = true
Task {
do {
let memberContact = try await apiCreateMemberContact(groupInfo.apiId, groupMember.groupMemberId)
await MainActor.run {
progressIndicator = false
chatModel.addChat(Chat(chatInfo: .direct(contact: memberContact)))
ItemsModel.shared.loadOpenChat(memberContact.id) {
dismissAllSheets(animated: true)
}
NetworkModel.shared.setContactNetworkStatus(memberContact, .connected)
}
} catch let error {
logger.error("createMemberContactButton apiCreateMemberContact error: \(responseError(error))")
let a = getErrorAlert(error, "Error creating member contact")
await MainActor.run {
progressIndicator = false
alert = .error(title: a.title, error: a.message)
}
}
NetworkModel.shared.setContactNetworkStatus(memberContact, .connected)
}
} catch let error {
logger.error("createMemberContactButton apiCreateMemberContact error: \(responseError(error))")
let a = getErrorAlert(error, "Error creating member contact")
await MainActor.run {
progressIndicator = false
alert = .error(title: a.title, error: a.message)
}
} else if connStats.ratchetSyncAllowed {
alert = .someAlert(alert: SomeAlert(
alert: Alert(
title: Text("Fix connection?"),
message: Text("Connection requires encryption renegotiation."),
primaryButton: .default(Text("Fix")) {
syncMemberConnection(force: false)
},
secondaryButton: .cancel()
),
id: "can't message member, fix connection"
))
} else {
alert = .someAlert(alert: SomeAlert(
alert: mkAlert(
title: "Can't message member",
message: "Encryption renegotiation in progress."
),
id: "can't message contact, encryption renegotiation in progress"
))
}
}
}
@@ -379,7 +439,7 @@ struct GroupMemberInfoView: View {
Text(Image(systemName: "checkmark.shield"))
.foregroundColor(theme.colors.secondary)
.font(.title2)
+ Text(" ")
+ textSpace
+ Text(mem.displayName)
.font(.largeTitle)
)
@@ -528,9 +588,14 @@ struct GroupMemberInfoView: View {
}
private func removeMemberAlert(_ mem: GroupMember) -> Alert {
Alert(
let label: LocalizedStringKey = (
groupInfo.businessChat == nil
? "Member will be removed from group - this cannot be undone!"
: "Member will be removed from chat - this cannot be undone!"
)
return Alert(
title: Text("Remove member?"),
message: Text("Member will be removed from group - this cannot be undone!"),
message: Text(label),
primaryButton: .destructive(Text("Remove")) {
Task {
do {
@@ -553,7 +618,15 @@ struct GroupMemberInfoView: View {
private func changeMemberRoleAlert(_ mem: GroupMember) -> Alert {
Alert(
title: Text("Change member role?"),
message: mem.memberCurrent ? Text("Member role will be changed to \"\(newRole.text)\". All group members will be notified.") : Text("Member role will be changed to \"\(newRole.text)\". The member will receive a new invitation."),
message: (
mem.memberCurrent
? (
groupInfo.businessChat == nil
? Text("Member role will be changed to \"\(newRole.text)\". All group members will be notified.")
: Text("Member role will be changed to \"\(newRole.text)\". All chat members will be notified.")
)
: Text("Member role will be changed to \"\(newRole.text)\". The member will receive a new invitation.")
),
primaryButton: .default(Text("Change")) {
Task {
do {
@@ -561,7 +634,7 @@ struct GroupMemberInfoView: View {
await MainActor.run {
_ = chatModel.upsertGroupMember(groupInfo, updatedMember)
}
} catch let error {
newRole = mem.memberRole
logger.error("apiMemberRole error: \(responseError(error))")
@@ -730,6 +803,7 @@ struct GroupMemberInfoView_Previews: PreviewProvider {
static var previews: some View {
GroupMemberInfoView(
groupInfo: GroupInfo.sampleData,
chat: Chat.sampleData,
groupMember: GMember.sampleData
)
}
@@ -20,9 +20,10 @@ struct GroupPreferencesView: View {
@EnvironmentObject var chatModel: ChatModel
@EnvironmentObject var theme: AppTheme
@Binding var groupInfo: GroupInfo
@State var preferences: FullGroupPreferences
@State var currentPreferences: FullGroupPreferences
@Binding var preferences: FullGroupPreferences
var currentPreferences: FullGroupPreferences
let creatingGroup: Bool
let savePreferences: () -> Void
@State private var showSaveDialogue = false
var body: some View {
@@ -38,7 +39,7 @@ struct GroupPreferencesView: View {
featureSection(.simplexLinks, $preferences.simplexLinks.enable, $preferences.simplexLinks.role)
featureSection(.history, $preferences.history.enable)
if groupInfo.canEdit {
if groupInfo.isOwner {
Section {
Button("Reset") { preferences = currentPreferences }
Button(saveText) { savePreferences() }
@@ -68,7 +69,10 @@ struct GroupPreferencesView: View {
savePreferences()
dismiss()
}
Button("Exit without saving") { dismiss() }
Button("Exit without saving") {
preferences = currentPreferences
dismiss()
}
}
}
@@ -77,7 +81,7 @@ struct GroupPreferencesView: View {
let color: Color = enableFeature.wrappedValue == .on ? .green : theme.colors.secondary
let icon = enableFeature.wrappedValue == .on ? feature.iconFilled : feature.icon
let timedOn = feature == .timedMessages && enableFeature.wrappedValue == .on
if groupInfo.canEdit {
if groupInfo.isOwner {
let enable = Binding(
get: { enableFeature.wrappedValue == .on },
set: { on, _ in enableFeature.wrappedValue = on ? .on : .off }
@@ -123,7 +127,7 @@ struct GroupPreferencesView: View {
}
}
} footer: {
Text(feature.enableDescription(enableFeature.wrappedValue, groupInfo.canEdit))
Text(feature.enableDescription(enableFeature.wrappedValue, groupInfo.isOwner))
.foregroundColor(theme.colors.secondary)
}
.onChange(of: enableFeature.wrappedValue) { enabled in
@@ -132,32 +136,16 @@ struct GroupPreferencesView: View {
}
}
}
private func savePreferences() {
Task {
do {
var gp = groupInfo.groupProfile
gp.groupPreferences = toGroupPreferences(preferences)
let gInfo = try await apiUpdateGroup(groupInfo.groupId, gp)
await MainActor.run {
groupInfo = gInfo
chatModel.updateGroup(gInfo)
currentPreferences = preferences
}
} catch {
logger.error("GroupPreferencesView apiUpdateGroup error: \(responseError(error))")
}
}
}
}
struct GroupPreferencesView_Previews: PreviewProvider {
static var previews: some View {
GroupPreferencesView(
groupInfo: Binding.constant(GroupInfo.sampleData),
preferences: FullGroupPreferences.sampleData,
preferences: Binding.constant(FullGroupPreferences.sampleData),
currentPreferences: FullGroupPreferences.sampleData,
creatingGroup: false
creatingGroup: false,
savePreferences: {}
)
}
}
@@ -110,10 +110,13 @@ struct GroupProfileView: View {
}
}
.onChange(of: chosenImage) { image in
if let image = image {
groupProfile.image = resizeImageToStrSize(cropToSquare(image), maxDataSize: 12500)
} else {
groupProfile.image = nil
Task {
let resized: String? = if let image {
await resizeImageToStrSize(cropToSquare(image), maxDataSize: 12500)
} else {
nil
}
await MainActor.run { groupProfile.image = resized }
}
}
.onAppear {
@@ -23,7 +23,7 @@ struct GroupWelcomeView: View {
var body: some View {
VStack {
if groupInfo.canEdit {
if groupInfo.isOwner && groupInfo.businessChat == nil {
editorView()
.modifier(BackButton(disabled: Binding.constant(false)) {
if welcomeTextUnchanged() {
+42 -21
View File
@@ -78,7 +78,7 @@ struct ReverseList<Content: View>: UIViewControllerRepresentable {
self.dataSource = UITableViewDiffableDataSource<Section, ChatItem>(
tableView: tableView
) { (tableView, indexPath, item) -> UITableViewCell? in
if indexPath.item > self.itemCount - 8, self.itemCount > 8 {
if indexPath.item > self.itemCount - 8 {
self.representer.loadPage()
}
let cell = tableView.dequeueReusableCell(withIdentifier: cellReuseId, for: indexPath)
@@ -107,9 +107,14 @@ struct ReverseList<Content: View>: UIViewControllerRepresentable {
name: notificationName,
object: nil
)
updateFloatingButtons
.throttle(for: 0.2, scheduler: DispatchQueue.main, latest: true)
.sink { self.updateVisibleItems() }
.throttle(for: 0.2, scheduler: DispatchQueue.global(qos: .background), latest: true)
.sink {
if let listState = DispatchQueue.main.sync(execute: { [weak self] in self?.getListState() }) {
ChatView.FloatingButtonModel.shared.updateOnListChange(listState)
}
}
.store(in: &bag)
}
@@ -203,25 +208,35 @@ struct ReverseList<Content: View>: UIViewControllerRepresentable {
updateFloatingButtons.send()
}
private func updateVisibleItems() {
let fbm = ChatView.FloatingButtonModel.shared
fbm.scrollOffset.send(tableView.contentOffset.y + InvertedTableView.inset)
fbm.visibleItems.send(
(tableView.indexPathsForVisibleRows ?? [])
.compactMap { indexPath -> String? in
guard let relativeFrame = tableView.superview?.convert(
tableView.rectForRow(at: indexPath),
from: tableView
) else { return nil }
// Checks that the cell is visible accounting for the added insets
let isVisible =
relativeFrame.maxY > InvertedTableView.inset &&
relativeFrame.minY < tableView.frame.height - InvertedTableView.inset
return indexPath.item < representer.items.count && isVisible
? representer.items[indexPath.item].viewId
: nil
func getListState() -> ListState? {
if let visibleRows = tableView.indexPathsForVisibleRows,
visibleRows.last?.item ?? 0 < representer.items.count {
let scrollOffset: Double = tableView.contentOffset.y + InvertedTableView.inset
let topItemDate: Date? =
if let lastVisible = visibleRows.last(where: { isVisible(indexPath: $0) }) {
representer.items[lastVisible.item].meta.itemTs
} else {
nil
}
)
let bottomItemId: ChatItem.ID? =
if let firstVisible = visibleRows.first(where: { isVisible(indexPath: $0) }) {
representer.items[firstVisible.item].id
} else {
nil
}
return (scrollOffset: scrollOffset, topItemDate: topItemDate, bottomItemId: bottomItemId)
}
return nil
}
private func isVisible(indexPath: IndexPath) -> Bool {
if let relativeFrame = tableView.superview?.convert(
tableView.rectForRow(at: indexPath),
from: tableView
) {
relativeFrame.maxY > InvertedTableView.inset &&
relativeFrame.minY < tableView.frame.height - InvertedTableView.inset
} else { false }
}
}
@@ -265,6 +280,12 @@ struct ReverseList<Content: View>: UIViewControllerRepresentable {
}
}
typealias ListState = (
scrollOffset: Double,
topItemDate: Date?,
bottomItemId: ChatItem.ID?
)
/// Manages ``ReverseList`` scrolling
class ReverseListScrollModel: ObservableObject {
/// Represents Scroll State of ``ReverseList``
@@ -32,13 +32,17 @@ struct SelectedItemsBottomToolbar: View {
var deleteItems: (Bool) -> Void
var moderateItems: () -> Void
//var shareItems: () -> Void
var forwardItems: () -> Void
@State var deleteEnabled: Bool = false
@State var deleteForEveryoneEnabled: Bool = false
@State var canModerate: Bool = false
@State var moderateEnabled: Bool = false
@State var allButtonsDisabled = false
@State var forwardEnabled: Bool = false
@State var deleteCountProhibited = false
@State var forwardCountProhibited = false
var body: some View {
VStack(spacing: 0) {
@@ -50,10 +54,11 @@ struct SelectedItemsBottomToolbar: View {
} label: {
Image(systemName: "trash")
.resizable()
.scaledToFit()
.frame(width: 20, height: 20, alignment: .center)
.foregroundColor(!deleteEnabled || allButtonsDisabled ? theme.colors.secondary: .red)
.foregroundColor(!deleteEnabled || deleteCountProhibited ? theme.colors.secondary: .red)
}
.disabled(!deleteEnabled || allButtonsDisabled)
.disabled(!deleteEnabled || deleteCountProhibited)
Spacer()
Button {
@@ -61,24 +66,24 @@ struct SelectedItemsBottomToolbar: View {
} label: {
Image(systemName: "flag")
.resizable()
.scaledToFit()
.frame(width: 20, height: 20, alignment: .center)
.foregroundColor(!moderateEnabled || allButtonsDisabled ? theme.colors.secondary : .red)
.foregroundColor(!moderateEnabled || deleteCountProhibited ? theme.colors.secondary : .red)
}
.disabled(!moderateEnabled || allButtonsDisabled)
.disabled(!moderateEnabled || deleteCountProhibited)
.opacity(canModerate ? 1 : 0)
Spacer()
Button {
//shareItems()
forwardItems()
} label: {
Image(systemName: "square.and.arrow.up")
Image(systemName: "arrowshape.turn.up.forward")
.resizable()
.scaledToFit()
.frame(width: 20, height: 20, alignment: .center)
.foregroundColor(allButtonsDisabled ? theme.colors.secondary : theme.colors.primary)
.foregroundColor(!forwardEnabled || forwardCountProhibited ? theme.colors.secondary : theme.colors.primary)
}
.disabled(allButtonsDisabled)
.opacity(0)
.disabled(!forwardEnabled || forwardCountProhibited)
}
.frame(maxHeight: .infinity)
.padding([.leading, .trailing], 12)
@@ -101,20 +106,22 @@ struct SelectedItemsBottomToolbar: View {
private func recheckItems(_ chatInfo: ChatInfo, _ chatItems: [ChatItem], _ selectedItems: Set<Int64>?) {
let count = selectedItems?.count ?? 0
allButtonsDisabled = count == 0 || count > 20
deleteCountProhibited = count == 0 || count > 200
forwardCountProhibited = count == 0 || count > 20
canModerate = possibleToModerate(chatInfo)
if let selected = selectedItems {
let me: Bool
let onlyOwnGroupItems: Bool
(deleteEnabled, deleteForEveryoneEnabled, me, onlyOwnGroupItems, selectedChatItems) = chatItems.reduce((true, true, true, true, [])) { (r, ci) in
(deleteEnabled, deleteForEveryoneEnabled, me, onlyOwnGroupItems, forwardEnabled, selectedChatItems) = chatItems.reduce((true, true, true, true, true, [])) { (r, ci) in
if selected.contains(ci.id) {
var (de, dee, me, onlyOwnGroupItems, sel) = r
var (de, dee, me, onlyOwnGroupItems, fe, sel) = r
de = de && ci.canBeDeletedForSelf
dee = dee && ci.meta.deletable && !ci.localNote
onlyOwnGroupItems = onlyOwnGroupItems && ci.chatDir == .groupSnd
me = me && ci.content.msgContent != nil && ci.memberToModerate(chatInfo) != nil
fe = fe && ci.content.msgContent != nil && ci.meta.itemDeleted == nil && !ci.isLiveDummy
sel.insert(ci.id) // we are collecting new selected items here to account for any changes in chat items list
return (de, dee, me, onlyOwnGroupItems, sel)
return (de, dee, me, onlyOwnGroupItems, fe, sel)
} else {
return r
}
@@ -10,7 +10,7 @@ import SwiftUI
struct ChatHelp: View {
@EnvironmentObject var chatModel: ChatModel
@Binding var showSettings: Bool
let dismissSettingsSheet: DismissAction
var body: some View {
ScrollView { chatHelp() }
@@ -23,7 +23,7 @@ struct ChatHelp: View {
VStack(alignment: .leading, spacing: 0) {
Text("To ask any questions and to receive updates:")
Button("connect to SimpleX Chat developers.") {
showSettings = false
dismissSettingsSheet()
DispatchQueue.main.async {
UIApplication.shared.open(simplexTeamURL)
}
@@ -42,7 +42,8 @@ struct ChatHelp: View {
Text("above, then choose:")
}
Text("**Add contact**: to create a new invitation link, or connect via a link you received.")
Text("**Create 1-time link**: to create and share a new invitation link.")
Text("**Scan / Paste link**: to connect via a link you received.")
Text("**Create group**: to create a new group.")
}
.padding(.top, 24)
@@ -61,8 +62,9 @@ struct ChatHelp: View {
}
struct ChatHelp_Previews: PreviewProvider {
@Environment(\.dismiss) static var mockDismiss
static var previews: some View {
@State var showSettings = false
return ChatHelp(showSettings: $showSettings)
ChatHelp(dismissSettingsSheet: mockDismiss)
}
}
@@ -404,8 +404,9 @@ struct ChatListNavLink: View {
}
private func deleteGroupAlert(_ groupInfo: GroupInfo) -> Alert {
Alert(
title: Text("Delete group?"),
let label: LocalizedStringKey = groupInfo.businessChat == nil ? "Delete group?" : "Delete chat?"
return Alert(
title: Text(label),
message: deleteGroupAlertMessage(groupInfo),
primaryButton: .destructive(Text("Delete")) {
Task { await deleteChat(chat) }
@@ -414,10 +415,6 @@ struct ChatListNavLink: View {
)
}
private func deleteGroupAlertMessage(_ groupInfo: GroupInfo) -> Text {
groupInfo.membership.memberCurrent ? Text("Group will be deleted for all members - this cannot be undone!") : Text("Group will be deleted for you - this cannot be undone!")
}
private func clearChatAlert() -> Alert {
Alert(
title: Text("Clear conversation?"),
@@ -441,9 +438,15 @@ struct ChatListNavLink: View {
}
private func leaveGroupAlert(_ groupInfo: GroupInfo) -> Alert {
Alert(
title: Text("Leave group?"),
message: Text("You will stop receiving messages from this group. Chat history will be preserved."),
let titleLabel: LocalizedStringKey = groupInfo.businessChat == nil ? "Leave group?" : "Leave chat?"
let messageLabel: LocalizedStringKey = (
groupInfo.businessChat == nil
? "You will stop receiving messages from this group. Chat history will be preserved."
: "You will stop receiving messages from this chat. Chat history will be preserved."
)
return Alert(
title: Text(titleLabel),
message: Text(messageLabel),
primaryButton: .destructive(Text("Leave")) {
Task { await leaveGroup(groupInfo.groupId) }
},
+138 -35
View File
@@ -9,22 +9,119 @@
import SwiftUI
import SimpleXChat
enum UserPickerSheet: Identifiable {
case address
case chatPreferences
case chatProfiles
case currentProfile
case useFromDesktop
case settings
var id: Self { self }
var navigationTitle: LocalizedStringKey {
switch self {
case .address: "SimpleX address"
case .chatPreferences: "Your preferences"
case .chatProfiles: "Your chat profiles"
case .currentProfile: "Your current profile"
case .useFromDesktop: "Connect to desktop"
case .settings: "Your settings"
}
}
}
class SaveableSettings: ObservableObject {
@Published var servers: ServerSettings = ServerSettings(currUserServers: [], userServers: [], serverErrors: [])
}
struct ServerSettings {
public var currUserServers: [UserOperatorServers]
public var userServers: [UserOperatorServers]
public var serverErrors: [UserServersError]
}
struct UserPickerSheetView: View {
let sheet: UserPickerSheet
@EnvironmentObject var chatModel: ChatModel
@StateObject private var ss = SaveableSettings()
@State private var loaded = false
var body: some View {
NavigationView {
ZStack {
if loaded, let currentUser = chatModel.currentUser {
switch sheet {
case .address:
UserAddressView(shareViaProfile: currentUser.addressShared)
case .chatPreferences:
PreferencesView(
profile: currentUser.profile,
preferences: currentUser.fullPreferences,
currentPreferences: currentUser.fullPreferences
)
case .chatProfiles:
UserProfilesView()
case .currentProfile:
UserProfile()
case .useFromDesktop:
ConnectDesktopView()
case .settings:
SettingsView()
}
}
Color.clear // Required for list background to be rendered during loading
}
.navigationTitle(sheet.navigationTitle)
.navigationBarTitleDisplayMode(.large)
.modifier(ThemedBackground(grouped: true))
}
.overlay {
if let la = chatModel.laRequest {
LocalAuthView(authRequest: la)
}
}
.task {
withAnimation(
.easeOut(duration: 0.1),
{ loaded = true }
)
}
.onDisappear {
if serversCanBeSaved(
ss.servers.currUserServers,
ss.servers.userServers,
ss.servers.serverErrors
) {
showAlert(
title: NSLocalizedString("Save servers?", comment: "alert title"),
buttonTitle: NSLocalizedString("Save", comment: "alert button"),
buttonAction: { saveServers($ss.servers.currUserServers, $ss.servers.userServers) },
cancelButton: true
)
}
}
.environmentObject(ss)
}
}
struct ChatListView: View {
@EnvironmentObject var chatModel: ChatModel
@EnvironmentObject var theme: AppTheme
@Binding var showSettings: Bool
@Binding var activeUserPickerSheet: UserPickerSheet?
@State private var searchMode = false
@FocusState private var searchFocussed
@State private var searchText = ""
@State private var searchShowingSimplexLink = false
@State private var searchChatFilteredBySimplexLink: String? = nil
@State private var userPickerVisible = false
@State private var showConnectDesktop = false
@State private var scrollToSearchBar = false
@State private var userPickerShown: Bool = false
@AppStorage(DEFAULT_SHOW_UNREAD_AND_FAVORITES) private var showUnreadAndFavorites = false
@AppStorage(GROUP_DEFAULT_ONE_HAND_UI, store: groupDefaults) private var oneHandUI = true
@AppStorage(DEFAULT_ONE_HAND_UI_CARD_SHOWN) private var oneHandUICardShown = false
@AppStorage(DEFAULT_ADDRESS_CREATION_CARD_SHOWN) private var addressCreationCardShown = false
@AppStorage(DEFAULT_TOOLBAR_MATERIAL) private var toolbarMaterial = ToolbarMaterial.defaultMaterial
var body: some View {
@@ -46,21 +143,23 @@ struct ChatListView: View {
),
destination: chatView
) { chatListView }
if userPickerVisible {
Rectangle().fill(.white.opacity(0.001)).onTapGesture {
withAnimation {
userPickerVisible.toggle()
}
}
.modifier(
Sheet(isPresented: $userPickerShown) {
UserPicker(userPickerShown: $userPickerShown, activeSheet: $activeUserPickerSheet)
}
)
.appSheet(
item: $activeUserPickerSheet,
onDismiss: { chatModel.laRequest = nil },
content: { UserPickerSheetView(sheet: $0) }
)
.onChange(of: activeUserPickerSheet) {
if $0 != nil {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
userPickerShown = false
}
}
UserPicker(
showSettings: $showSettings,
showConnectDesktop: $showConnectDesktop,
userPickerVisible: $userPickerVisible
)
}
.sheet(isPresented: $showConnectDesktop) {
ConnectDesktopView()
}
}
@@ -73,7 +172,7 @@ struct ChatListView: View {
.navigationBarHidden(searchMode || oneHandUI)
}
.scaleEffect(x: 1, y: oneHandUI ? -1 : 1, anchor: .center)
.onDisappear() { withAnimation { userPickerVisible = false } }
.onDisappear() { activeUserPickerSheet = nil }
.refreshable {
AlertManager.shared.showAlert(Alert(
title: Text("Reconnect servers?"),
@@ -164,7 +263,7 @@ struct ChatListView: View {
let user = chatModel.currentUser ?? User.sampleData
ZStack(alignment: .topTrailing) {
ProfileImage(imageStr: user.image, size: 32, color: Color(uiColor: .quaternaryLabel))
.padding(.trailing, 4)
.padding([.top, .trailing], 3)
let allRead = chatModel.users
.filter { u in !u.user.activeUser && !u.user.hidden }
.allSatisfy { u in u.unreadCount == 0 }
@@ -173,13 +272,7 @@ struct ChatListView: View {
}
}
.onTapGesture {
if chatModel.users.filter({ u in u.user.activeUser || !u.user.hidden }).count > 1 {
withAnimation {
userPickerVisible.toggle()
}
} else {
showSettings = true
}
userPickerShown = true
}
}
@@ -211,12 +304,6 @@ struct ChatListView: View {
.padding(.top, oneHandUI ? 8 : 0)
.id("searchBar")
}
if !oneHandUICardShown {
OneHandUICard()
.scaleEffect(x: 1, y: oneHandUI ? -1 : 1, anchor: .center)
.listRowSeparator(.hidden)
.listRowBackground(Color.clear)
}
if #available(iOS 16.0, *) {
ForEach(cs, id: \.viewId) { chat in
ChatListNavLink(chat: chat)
@@ -242,6 +329,20 @@ struct ChatListView: View {
.disabled(chatModel.chatRunning != true || chatModel.deletedChats.contains(chat.chatInfo.id))
}
}
if !oneHandUICardShown {
OneHandUICard()
.padding(.vertical, 6)
.scaleEffect(x: 1, y: oneHandUI ? -1 : 1, anchor: .center)
.listRowSeparator(.hidden)
.listRowBackground(Color.clear)
}
if !addressCreationCardShown {
AddressCreationCard()
.padding(.vertical, 6)
.scaleEffect(x: 1, y: oneHandUI ? -1 : 1, anchor: .center)
.listRowSeparator(.hidden)
.listRowBackground(Color.clear)
}
}
.listStyle(.plain)
.onChange(of: chatModel.chatId) { currentChatId in
@@ -269,7 +370,7 @@ struct ChatListView: View {
}
}
private func unreadBadge(_ text: Text? = Text(" "), size: CGFloat = 18) -> some View {
private func unreadBadge(size: CGFloat = 18) -> some View {
Circle()
.frame(width: size, height: size)
.foregroundColor(theme.colors.primary)
@@ -374,7 +475,7 @@ struct SubsStatusIndicator: View {
private func startTask() {
task = Task {
while !Task.isCancelled {
if AppChatState.shared.value == .active {
if AppChatState.shared.value == .active, ChatModel.shared.chatRunning == true {
do {
let (subs, hasSess) = try await getAgentSubsTotal()
await MainActor.run {
@@ -516,6 +617,8 @@ func chatStoppedIcon() -> some View {
}
struct ChatListView_Previews: PreviewProvider {
@State static var userPickerSheet: UserPickerSheet? = .none
static var previews: some View {
let chatModel = ChatModel()
chatModel.updateChats([
@@ -534,9 +637,9 @@ struct ChatListView_Previews: PreviewProvider {
])
return Group {
ChatListView(showSettings: Binding.constant(false))
ChatListView(activeUserPickerSheet: $userPickerSheet)
.environmentObject(chatModel)
ChatListView(showSettings: Binding.constant(false))
ChatListView(activeUserPickerSheet: $userPickerSheet)
.environmentObject(ChatModel())
}
}
@@ -35,11 +35,16 @@ struct ChatPreviewView: View {
}
.padding(.leading, 4)
let chatTs = if let cItem {
cItem.meta.itemTs
} else {
chat.chatInfo.chatTs
}
VStack(spacing: 0) {
HStack(alignment: .top) {
chatPreviewTitle()
Spacer()
(cItem?.timestampText ?? formatTimestampText(chat.chatInfo.chatTs))
(formatTimestampText(chatTs))
.font(.subheadline)
.frame(minWidth: 60, alignment: .trailing)
.foregroundColor(theme.colors.secondary)
@@ -167,7 +172,7 @@ struct ChatPreviewView: View {
}
private var verifiedIcon: Text {
(Text(Image(systemName: "checkmark.shield")) + Text(" "))
(Text(Image(systemName: "checkmark.shield")) + textSpace)
.foregroundColor(theme.colors.secondary)
.baselineOffset(1)
.kerning(-2)
@@ -227,12 +232,12 @@ struct ChatPreviewView: View {
+ messageText(msg, parseSimpleXMarkdown(msg), nil, preview: true, showSecrets: false, secondaryColor: theme.colors.secondary)
func image(_ s: String, color: Color = Color(uiColor: .tertiaryLabel)) -> Text {
Text(Image(systemName: s)).foregroundColor(color) + Text(" ")
Text(Image(systemName: s)).foregroundColor(color) + textSpace
}
func attachment() -> Text {
switch draft.preview {
case let .filePreview(fileName, _): return image("doc.fill") + Text(fileName) + Text(" ")
case let .filePreview(fileName, _): return image("doc.fill") + Text(fileName) + textSpace
case .mediaPreviews: return image("photo")
case let .voicePreview(_, duration): return image("play.fill") + Text(durationText(duration))
default: return Text("")
@@ -302,7 +307,7 @@ struct ChatPreviewView: View {
case let .link(_, preview):
smallContentPreview(size: dynamicMediaSize) {
ZStack(alignment: .topTrailing) {
Image(uiImage: UIImage(base64Encoded: preview.image) ?? UIImage(systemName: "arrow.up.right")!)
Image(uiImage: imageFromBase64(preview.image) ?? UIImage(systemName: "arrow.up.right")!)
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: dynamicMediaSize, height: dynamicMediaSize)
@@ -323,12 +328,12 @@ struct ChatPreviewView: View {
}
case let .image(_, image):
smallContentPreview(size: dynamicMediaSize) {
CIImageView(chatItem: ci, preview: UIImage(base64Encoded: image), maxWidth: dynamicMediaSize, smallView: true, showFullScreenImage: $showFullscreenGallery)
CIImageView(chatItem: ci, preview: imageFromBase64(image), maxWidth: dynamicMediaSize, smallView: true, showFullScreenImage: $showFullscreenGallery)
.environmentObject(ReverseListScrollModel())
}
case let .video(_,image, duration):
smallContentPreview(size: dynamicMediaSize) {
CIVideoView(chatItem: ci, preview: UIImage(base64Encoded: image), duration: duration, maxWidth: dynamicMediaSize, videoWidth: nil, smallView: true, showFullscreenPlayer: $showFullscreenGallery)
CIVideoView(chatItem: ci, preview: imageFromBase64(image), duration: duration, maxWidth: dynamicMediaSize, videoWidth: nil, smallView: true, showFullscreenPlayer: $showFullscreenGallery)
.environmentObject(ReverseListScrollModel())
}
case let .voice(_, duration):
@@ -362,11 +367,11 @@ struct ChatPreviewView: View {
case .sndErrorAuth, .sndError:
return Text(Image(systemName: "multiply"))
.font(.caption)
.foregroundColor(.red) + Text(" ")
.foregroundColor(.red) + textSpace
case .sndWarning:
return Text(Image(systemName: "exclamationmark.triangle.fill"))
.font(.caption)
.foregroundColor(.orange) + Text(" ")
.foregroundColor(.orange) + textSpace
default: return Text("")
}
}
@@ -32,7 +32,6 @@ struct OneHandUICard: View {
.background(theme.appColors.sentMessage)
.cornerRadius(12)
.frame(height: dynamicSize(userFont).rowHeight)
.padding(.vertical, 12)
.alert(isPresented: $showOneHandUIAlert) {
Alert(
title: Text("Reachable chat toolbar"),
@@ -407,12 +407,18 @@ struct ServersSummaryView: View {
struct SubscriptionStatusIndicatorView: View {
@EnvironmentObject var m: ChatModel
@EnvironmentObject var theme: AppTheme
var subs: SMPServerSubs
var hasSess: Bool
var body: some View {
let onionHosts = networkUseOnionHostsGroupDefault.get()
let (color, variableValue, opacity, _) = subscriptionStatusColorAndPercentage(m.networkInfo.online, onionHosts, subs, hasSess)
let (color, variableValue, opacity, _) = subscriptionStatusColorAndPercentage(
online: m.networkInfo.online,
usesProxy: networkUseOnionHostsGroupDefault.get() != .no || groupDefaults.string(forKey: GROUP_DEFAULT_NETWORK_SOCKS_PROXY) != nil,
subs: subs,
hasSess: hasSess,
primaryColor: theme.colors.primary
)
if #available(iOS 16.0, *) {
Image(systemName: "dot.radiowaves.up.forward", variableValue: variableValue)
.foregroundColor(color)
@@ -425,26 +431,32 @@ struct SubscriptionStatusIndicatorView: View {
struct SubscriptionStatusPercentageView: View {
@EnvironmentObject var m: ChatModel
@EnvironmentObject var theme: AppTheme
var subs: SMPServerSubs
var hasSess: Bool
var body: some View {
let onionHosts = networkUseOnionHostsGroupDefault.get()
let (_, _, _, statusPercent) = subscriptionStatusColorAndPercentage(m.networkInfo.online, onionHosts, subs, hasSess)
let (_, _, _, statusPercent) = subscriptionStatusColorAndPercentage(
online: m.networkInfo.online,
usesProxy: networkUseOnionHostsGroupDefault.get() != .no || groupDefaults.string(forKey: GROUP_DEFAULT_NETWORK_SOCKS_PROXY) != nil,
subs: subs,
hasSess: hasSess,
primaryColor: theme.colors.primary
)
Text(verbatim: "\(Int(floor(statusPercent * 100)))%")
.foregroundColor(.secondary)
.font(.caption)
}
}
func subscriptionStatusColorAndPercentage(_ online: Bool, _ onionHosts: OnionHosts, _ subs: SMPServerSubs, _ hasSess: Bool) -> (Color, Double, Double, Double) {
func subscriptionStatusColorAndPercentage(online: Bool, usesProxy: Bool, subs: SMPServerSubs, hasSess: Bool, primaryColor: Color) -> (Color, Double, Double, Double) {
func roundedToQuarter(_ n: Double) -> Double {
n >= 1 ? 1
: n <= 0 ? 0
: (n * 4).rounded() / 4
}
let activeColor: Color = onionHosts == .require ? .indigo : .accentColor
let activeColor: Color = usesProxy ? .indigo : primaryColor
let noConnColorAndPercent: (Color, Double, Double, Double) = (Color(uiColor: .tertiaryLabel), 1, 1, 0)
let activeSubsRounded = roundedToQuarter(subs.shareOfActive)
@@ -479,15 +491,6 @@ struct SMPServerSummaryView: View {
Section("Server address") {
Text(summary.smpServer)
.textSelection(.enabled)
if summary.known == true {
NavigationLink {
ProtocolServersView(serverProtocol: .smp)
.navigationTitle("Your SMP servers")
.modifier(ThemedBackground(grouped: true))
} label: {
Text("Open server settings")
}
}
}
if let stats = summary.stats {
@@ -584,7 +587,7 @@ struct SMPStatsView: View {
} header: {
Text("Statistics")
} footer: {
Text("Starting from \(localTimestamp(statsStartedAt)).") + Text("\n") + Text("All data is private to your device.")
Text("Starting from \(localTimestamp(statsStartedAt)).") + Text("\n") + Text("All data is kept private on your device.")
}
}
}
@@ -667,15 +670,6 @@ struct XFTPServerSummaryView: View {
Section("Server address") {
Text(summary.xftpServer)
.textSelection(.enabled)
if summary.known == true {
NavigationLink {
ProtocolServersView(serverProtocol: .xftp)
.navigationTitle("Your XFTP servers")
.modifier(ThemedBackground(grouped: true))
} label: {
Text("Open server settings")
}
}
}
if let stats = summary.stats {
@@ -709,7 +703,7 @@ struct XFTPStatsView: View {
} header: {
Text("Statistics")
} footer: {
Text("Starting from \(localTimestamp(statsStartedAt)).") + Text("\n") + Text("All data is private to your device.")
Text("Starting from \(localTimestamp(statsStartedAt)).") + Text("\n") + Text("All data is kept private on your device.")
}
}
}
+243 -141
View File
@@ -8,179 +8,281 @@ import SimpleXChat
struct UserPicker: View {
@EnvironmentObject var m: ChatModel
@Environment(\.scenePhase) var scenePhase
@EnvironmentObject var theme: AppTheme
@Binding var showSettings: Bool
@Binding var showConnectDesktop: Bool
@Binding var userPickerVisible: Bool
@State var scrollViewContentSize: CGSize = .zero
@State var disableScrolling: Bool = true
private let menuButtonHeight: CGFloat = 68
@State var chatViewNameWidth: CGFloat = 0
@Environment(\.dynamicTypeSize) private var userFont: DynamicTypeSize
@Environment(\.scenePhase) private var scenePhase: ScenePhase
@Environment(\.colorScheme) private var colorScheme: ColorScheme
@Binding var userPickerShown: Bool
@Binding var activeSheet: UserPickerSheet?
@State private var currentUser: Int64?
@State private var switchingProfile = false
@State private var frameWidth: CGFloat = 0
@State private var resetScroll = ResetScrollAction()
// Inset grouped list dimensions
private let imageSize: CGFloat = 44
private let rowPadding: CGFloat = 16
private let rowVerticalPadding: CGFloat = 11
private let sectionSpacing: CGFloat = 35
private var sectionHorizontalPadding: CGFloat { frameWidth > 375 ? 20 : 16 }
private let sectionShape = RoundedRectangle(cornerRadius: 10, style: .continuous)
var body: some View {
VStack {
Spacer().frame(height: 1)
let otherUsers: [UserInfo] = m.users
.filter { u in !u.user.hidden && u.user.userId != m.currentUser?.userId }
.sorted(using: KeyPathComparator<UserInfo>(\.user.activeOrder, order: .reverse))
let sectionWidth = max(frameWidth - sectionHorizontalPadding * 2, 0)
let currentUserWidth = max(frameWidth - sectionHorizontalPadding - rowPadding * 2 - 14 - imageSize, 0)
let stopped = m.chatRunning != true
VStack(spacing: sectionSpacing) {
if let user = m.currentUser {
StickyScrollView(resetScroll: $resetScroll) {
HStack(spacing: rowPadding) {
HStack {
ProfileImage(imageStr: user.image, size: imageSize, color: Color(uiColor: .tertiarySystemGroupedBackground))
.padding(.trailing, 6)
profileName(user).lineLimit(1)
}
.padding(rowPadding)
.frame(width: otherUsers.isEmpty ? sectionWidth : currentUserWidth, alignment: .leading)
.modifier(ListRow { activeSheet = .currentProfile })
.clipShape(sectionShape)
.disabled(stopped)
.opacity(stopped ? 0.4 : 1)
ForEach(otherUsers) { u in
userView(u, size: imageSize)
.frame(maxWidth: sectionWidth * 0.618)
.fixedSize()
.disabled(stopped)
.opacity(stopped ? 0.4 : 1)
}
}
.padding(.horizontal, sectionHorizontalPadding)
}
.frame(height: 2 * rowPadding + imageSize)
.padding(.top, sectionSpacing)
.overlay(DetermineWidth())
.onPreferenceChange(DetermineWidth.Key.self) { frameWidth = $0 }
}
VStack(spacing: 0) {
ScrollView {
ScrollViewReader { sp in
let users = m.users
.filter({ u in u.user.activeUser || !u.user.hidden })
.sorted { u, _ in u.user.activeUser }
VStack(spacing: 0) {
ForEach(users) { u in
userView(u)
Divider()
if u.user.activeUser { Divider() }
openSheetOnTap("qrcode", title: m.userAddress == nil ? "Create SimpleX address" : "Your SimpleX address", sheet: .address, disabled: stopped)
openSheetOnTap("switch.2", title: "Chat preferences", sheet: .chatPreferences, disabled: stopped)
openSheetOnTap("person.crop.rectangle.stack", title: "Your chat profiles", sheet: .chatProfiles, disabled: stopped)
openSheetOnTap("desktopcomputer", title: "Use from desktop", sheet: .useFromDesktop, disabled: stopped)
ZStack(alignment: .trailing) {
openSheetOnTap("gearshape", title: "Settings", sheet: .settings, showDivider: false)
Image(systemName: colorScheme == .light ? "sun.max" : "moon.fill")
.resizable()
.scaledToFit()
.symbolRenderingMode(.monochrome)
.foregroundColor(theme.colors.secondary)
.frame(maxWidth: 20, maxHeight: .infinity)
.padding(.horizontal, rowPadding)
.background(Color(.systemBackground).opacity(0.01))
.onTapGesture {
if (colorScheme == .light) {
ThemeManager.applyTheme(systemDarkThemeDefault.get())
} else {
ThemeManager.applyTheme(DefaultTheme.LIGHT.themeName)
}
}
.overlay {
GeometryReader { geo -> Color in
DispatchQueue.main.async {
scrollViewContentSize = geo.size
let scenes = UIApplication.shared.connectedScenes
if let windowScene = scenes.first as? UIWindowScene {
let layoutFrame = windowScene.windows[0].safeAreaLayoutGuide.layoutFrame
disableScrolling = scrollViewContentSize.height + menuButtonHeight + 10 < layoutFrame.height
}
}
return Color.clear
}
.onLongPressGesture {
ThemeManager.applyTheme(DefaultTheme.SYSTEM_THEME_NAME)
}
.onChange(of: userPickerVisible) { visible in
if visible, let u = users.first {
sp.scrollTo(u.id)
}
}
}
}
.simultaneousGesture(DragGesture(minimumDistance: disableScrolling ? 0 : 10000000))
.frame(maxHeight: scrollViewContentSize.height)
menuButton("Use from desktop", icon: "desktopcomputer") {
showConnectDesktop = true
withAnimation {
userPickerVisible.toggle()
}
}
Divider()
menuButton("Settings", icon: "gearshape") {
showSettings = true
withAnimation {
userPickerVisible.toggle()
}
}
}
.clipShape(sectionShape)
.padding(.horizontal, sectionHorizontalPadding)
.padding(.bottom, sectionSpacing)
}
.clipShape(RoundedRectangle(cornerRadius: 16))
.background(
Rectangle()
.fill(theme.colors.surface)
.cornerRadius(16)
.shadow(color: .black.opacity(0.12), radius: 24, x: 0, y: 0)
)
.onPreferenceChange(DetermineWidth.Key.self) { chatViewNameWidth = $0 }
.frame(maxWidth: chatViewNameWidth > 0 ? min(300, chatViewNameWidth + 130) : 300)
.padding(8)
.opacity(userPickerVisible ? 1.0 : 0.0)
.onAppear {
// This check prevents the call of listUsers after the app is suspended, and the database is closed.
if case .active = scenePhase {
Task {
do {
let users = try await listUsersAsync()
await MainActor.run { m.users = users }
} catch {
logger.error("Error loading users \(responseError(error))")
}
}
}
}
}
private func userView(_ u: UserInfo) -> some View {
let user = u.user
return Button(action: {
if user.activeUser {
showSettings = true
withAnimation {
userPickerVisible.toggle()
}
} else {
// This check prevents the call of listUsers after the app is suspended, and the database is closed.
if case .active = scenePhase {
currentUser = m.currentUser?.userId
Task {
do {
try await changeActiveUserAsync_(user.userId, viewPwd: nil)
await MainActor.run { userPickerVisible = false }
} catch {
let users = try await listUsersAsync()
await MainActor.run {
AlertManager.shared.showAlertMsg(
title: "Error switching profile!",
message: "Error: \(responseError(error))"
)
m.users = users
currentUser = m.currentUser?.userId
}
} catch {
logger.error("Error loading users \(responseError(error))")
}
}
}
}, label: {
HStack(spacing: 0) {
ProfileImage(imageStr: user.image, size: 44, color: Color(uiColor: .tertiarySystemFill))
.padding(.trailing, 12)
Text(user.chatViewName)
.fontWeight(user.activeUser ? .medium : .regular)
.foregroundColor(theme.colors.onBackground)
.overlay(DetermineWidth())
Spacer()
if user.activeUser {
Image(systemName: "checkmark")
} else if u.unreadCount > 0 {
unreadCounter(u.unreadCount, color: user.showNtfs ? theme.colors.primary : theme.colors.secondary)
} else if !user.showNtfs {
Image(systemName: "speaker.slash")
}
.onChange(of: userPickerShown) {
if !$0 { resetScroll() }
}
.modifier(ThemedBackground(grouped: true))
.disabled(switchingProfile)
}
private func userView(_ u: UserInfo, size: CGFloat) -> some View {
HStack {
ZStack(alignment: .topTrailing) {
ProfileImage(imageStr: u.user.image, size: size, color: Color(uiColor: .tertiarySystemGroupedBackground))
if (u.unreadCount > 0) {
UnreadBadge(userInfo: u).offset(x: 4, y: -4)
}
}
.padding(.trailing)
.padding([.leading, .vertical], 12)
})
.buttonStyle(PressedButtonStyle(defaultColor: theme.colors.surface, pressedColor: Color(uiColor: .secondarySystemFill)))
}
private func menuButton(_ title: LocalizedStringKey, icon: String, action: @escaping () -> Void) -> some View {
Button(action: action) {
HStack(spacing: 0) {
Text(title)
.overlay(DetermineWidth())
Spacer()
Image(systemName: icon)
.symbolRenderingMode(.monochrome)
.foregroundColor(theme.colors.secondary)
}
.padding(.horizontal)
.padding(.vertical, 22)
.frame(height: menuButtonHeight)
.padding(.trailing, 6)
Text(u.user.displayName).font(.title2).lineLimit(1)
}
.padding(rowPadding)
.modifier(ListRow {
switchingProfile = true
Task {
do {
try await changeActiveUserAsync_(u.user.userId, viewPwd: nil)
await MainActor.run {
switchingProfile = false
userPickerShown = false
}
} catch {
await MainActor.run {
switchingProfile = false
showAlert(
NSLocalizedString("Error switching profile!", comment: "alertTitle"),
message: String.localizedStringWithFormat(NSLocalizedString("Error: %@", comment: "alert message"), responseError(error))
)
}
}
}
})
.clipShape(sectionShape)
}
private func openSheetOnTap(_ icon: String, title: LocalizedStringKey, sheet: UserPickerSheet, showDivider: Bool = true, disabled: Bool = false) -> some View {
ZStack(alignment: .bottom) {
settingsRow(icon, color: theme.colors.secondary) {
Text(title).foregroundColor(.primary).opacity(disabled ? 0.4 : 1)
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal, rowPadding)
.padding(.vertical, rowVerticalPadding)
.modifier(ListRow { activeSheet = sheet })
.disabled(disabled)
if showDivider {
Divider().padding(.leading, 52)
}
}
.buttonStyle(PressedButtonStyle(defaultColor: theme.colors.surface, pressedColor: Color(uiColor: .secondarySystemFill)))
}
}
private func unreadCounter(_ unread: Int, color: Color) -> some View {
unreadCountText(unread)
.font(.caption)
.foregroundColor(.white)
.padding(.horizontal, 4)
.frame(minWidth: 18, minHeight: 18)
.background(color)
.cornerRadius(10)
struct UnreadBadge: View {
var userInfo: UserInfo
@EnvironmentObject var theme: AppTheme
@Environment(\.dynamicTypeSize) private var userFont: DynamicTypeSize
var body: some View {
let size = dynamicSize(userFont).chatInfoSize
unreadCountText(userInfo.unreadCount)
.font(userFont <= .xxxLarge ? .caption : .caption2)
.foregroundColor(.white)
.padding(.horizontal, dynamicSize(userFont).unreadPadding)
.frame(minWidth: size, minHeight: size)
.background(userInfo.user.showNtfs ? theme.colors.primary : theme.colors.secondary)
.cornerRadius(dynamicSize(userFont).unreadCorner)
}
}
struct ListRow: ViewModifier {
@Environment(\.colorScheme) private var colorScheme: ColorScheme
@State private var touchDown = false
let action: () -> Void
func body(content: Content) -> some View {
ZStack {
elevatedSecondarySystemGroupedBackground
Color(.systemGray4).opacity(touchDown ? 1 : 0)
content
TouchOverlay(touchDown: $touchDown, action: action)
}
}
var elevatedSecondarySystemGroupedBackground: Color {
switch colorScheme {
case .dark: Color(0xFF2C2C2E)
default: Color(0xFFFFFFFF)
}
}
struct TouchOverlay: UIViewRepresentable {
@Binding var touchDown: Bool
let action: () -> Void
func makeUIView(context: Context) -> TouchView {
let touchView = TouchView()
let gesture = UILongPressGestureRecognizer(
target: touchView,
action: #selector(touchView.longPress(gesture:))
)
gesture.delegate = touchView
gesture.minimumPressDuration = 0
touchView.addGestureRecognizer(gesture)
return touchView
}
func updateUIView(_ touchView: TouchView, context: Context) {
touchView.representer = self
}
class TouchView: UIView, UIGestureRecognizerDelegate {
var representer: TouchOverlay?
private var startLocation: CGPoint?
private var task: Task<Void, Never>?
@objc
func longPress(gesture: UILongPressGestureRecognizer) {
switch gesture.state {
case .began:
startLocation = gesture.location(in: nil)
task = Task {
do {
try await Task.sleep(nanoseconds: 200_000000)
await MainActor.run { representer?.touchDown = true }
} catch { }
}
case .ended:
if hitTest(gesture.location(in: self), with: nil) == self {
representer?.action()
}
task?.cancel()
representer?.touchDown = false
case .changed:
if let startLocation {
let location = gesture.location(in: nil)
let dx = location.x - startLocation.x
let dy = location.y - startLocation.y
if sqrt(pow(dx, 2) + pow(dy, 2)) > 10 { gesture.state = .failed }
}
case .cancelled, .failed:
task?.cancel()
representer?.touchDown = false
default: break
}
}
func gestureRecognizer(
_: UIGestureRecognizer,
shouldRecognizeSimultaneouslyWith: UIGestureRecognizer
) -> Bool { true }
}
}
}
struct UserPicker_Previews: PreviewProvider {
static var previews: some View {
@State var activeSheet: UserPickerSheet?
let m = ChatModel()
m.users = [UserInfo.sampleData, UserInfo.sampleData]
return UserPicker(
showSettings: Binding.constant(false),
showConnectDesktop: Binding.constant(false),
userPickerVisible: Binding.constant(true)
userPickerShown: .constant(true),
activeSheet: $activeSheet
)
.environmentObject(m)
}
@@ -151,7 +151,7 @@ struct ContactListNavLink: View {
}
private var verifiedIcon: Text {
(Text(Image(systemName: "checkmark.shield")) + Text(" "))
(Text(Image(systemName: "checkmark.shield")) + textSpace)
.foregroundColor(.secondary)
.baselineOffset(1)
.kerning(-2)
@@ -1,68 +0,0 @@
//
// ChatArchiveView.swift
// SimpleXChat
//
// Created by Evgeny on 23/06/2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
import SwiftUI
import SimpleXChat
struct ChatArchiveView: View {
@EnvironmentObject var theme: AppTheme
var archiveName: String
@AppStorage(DEFAULT_CHAT_ARCHIVE_NAME) private var chatArchiveName: String?
@AppStorage(DEFAULT_CHAT_ARCHIVE_TIME) private var chatArchiveTime: Double = 0
@State private var showDeleteAlert = false
var body: some View {
let fileUrl = getDocumentsDirectory().appendingPathComponent(archiveName)
let fileTs = chatArchiveTimeDefault.get()
List {
Section {
settingsRow("square.and.arrow.up", color: theme.colors.secondary) {
Button {
showShareSheet(items: [fileUrl])
} label: {
Text("Save archive")
}
}
settingsRow("trash", color: theme.colors.secondary) {
Button {
showDeleteAlert = true
} label: {
Text("Delete archive").foregroundColor(.red)
}
}
} header: {
Text("Chat archive")
.foregroundColor(theme.colors.secondary)
} footer: {
Text("Created on \(fileTs)")
.foregroundColor(theme.colors.secondary)
}
}
.alert(isPresented: $showDeleteAlert) {
Alert(
title: Text("Delete chat archive?"),
primaryButton: .destructive(Text("Delete")) {
do {
try FileManager.default.removeItem(atPath: fileUrl.path)
chatArchiveName = nil
chatArchiveTime = 0
} catch let error {
logger.error("removeItem error \(String(describing: error))")
}
},
secondaryButton: .cancel()
)
}
}
}
struct ChatArchiveView_Previews: PreviewProvider {
static var previews: some View {
ChatArchiveView(archiveName: "")
}
}
@@ -48,6 +48,8 @@ struct DatabaseEncryptionView: View {
@State private var confirmNewKey = ""
@State private var currentKeyShown = false
let stopChatRunBlockStartChat: (Binding<Bool>, @escaping () async throws -> Bool) -> Void
var body: some View {
ZStack {
List {
@@ -134,46 +136,61 @@ struct DatabaseEncryptionView: View {
.onAppear {
if initialRandomDBPassphrase { currentKey = kcDatabasePassword.get() ?? "" }
}
.disabled(m.chatRunning != false)
.disabled(progressIndicator)
.alert(item: $alert) { item in databaseEncryptionAlert(item) }
}
private func encryptDatabase() {
progressIndicator = true
Task {
do {
encryptionStartedDefault.set(true)
encryptionStartedAtDefault.set(Date.now)
if !m.chatDbChanged {
try apiSaveAppSettings(settings: AppSettings.current.prepareForExport())
}
try await apiStorageEncryption(currentKey: currentKey, newKey: newKey)
encryptionStartedDefault.set(false)
initialRandomDBPassphraseGroupDefault.set(false)
if migration {
storeDBPassphraseGroupDefault.set(useKeychain)
}
if useKeychain {
if kcDatabasePassword.set(newKey) {
await resetFormAfterEncryption(true)
await operationEnded(.databaseEncrypted)
} else {
await resetFormAfterEncryption()
await operationEnded(.error(title: "Keychain error", error: "Error saving passphrase to keychain"))
}
} else {
if migration {
removePassphraseFromKeyChain()
}
await resetFormAfterEncryption()
private func encryptDatabaseAsync() async -> Bool {
await MainActor.run {
progressIndicator = true
}
do {
encryptionStartedDefault.set(true)
encryptionStartedAtDefault.set(Date.now)
if !m.chatDbChanged {
try apiSaveAppSettings(settings: AppSettings.current.prepareForExport())
}
try await apiStorageEncryption(currentKey: currentKey, newKey: newKey)
encryptionStartedDefault.set(false)
initialRandomDBPassphraseGroupDefault.set(false)
if migration {
storeDBPassphraseGroupDefault.set(useKeychain)
}
if useKeychain {
if kcDatabasePassword.set(newKey) {
await resetFormAfterEncryption(true)
await operationEnded(.databaseEncrypted)
}
} catch let error {
if case .chatCmdError(_, .errorDatabase(.errorExport(.errorNotADatabase))) = error as? ChatResponse {
await operationEnded(.currentPassphraseError)
} else {
await operationEnded(.error(title: "Error encrypting database", error: "\(responseError(error))"))
await resetFormAfterEncryption()
await operationEnded(.error(title: "Keychain error", error: "Error saving passphrase to keychain"))
}
} else {
if migration {
removePassphraseFromKeyChain()
}
await resetFormAfterEncryption()
await operationEnded(.databaseEncrypted)
}
return true
} catch let error {
if case .chatCmdError(_, .errorDatabase(.errorExport(.errorNotADatabase))) = error as? ChatResponse {
await operationEnded(.currentPassphraseError)
} else {
await operationEnded(.error(title: "Error encrypting database", error: "\(responseError(error))"))
}
return false
}
}
private func encryptDatabase() {
// it will try to stop and start the chat in case of: non-migration && successful encryption. In migration the chat will remain stopped
if migration {
Task {
await encryptDatabaseAsync()
}
} else {
stopChatRunBlockStartChat($progressIndicator) {
return await encryptDatabaseAsync()
}
}
}
@@ -371,6 +388,6 @@ func validKey(_ s: String) -> Bool {
struct DatabaseEncryptionView_Previews: PreviewProvider {
static var previews: some View {
DatabaseEncryptionView(useKeychain: Binding.constant(true), migration: false)
DatabaseEncryptionView(useKeychain: Binding.constant(true), migration: false, stopChatRunBlockStartChat: { _, _ in true })
}
}
@@ -11,6 +11,7 @@ import SimpleXChat
struct DatabaseErrorView: View {
@EnvironmentObject var m: ChatModel
@EnvironmentObject var theme: AppTheme
@State var status: DBMigrationResult
@State private var dbKey = ""
@State private var storedDBKey = kcDatabasePassword.get()
@@ -28,23 +29,39 @@ struct DatabaseErrorView: View {
}
@ViewBuilder private func databaseErrorView() -> some View {
VStack(alignment: .leading, spacing: 16) {
VStack(alignment: .center, spacing: 20) {
switch status {
case let .errorNotADatabase(dbFile):
if useKeychain && storedDBKey != nil && storedDBKey != "" {
titleText("Wrong database passphrase")
Text("Database passphrase is different from saved in the keychain.")
.font(.callout)
.foregroundColor(theme.colors.secondary)
.multilineTextAlignment(.center)
.padding(.horizontal, 25)
databaseKeyField(onSubmit: saveAndRunChat)
saveAndOpenButton()
fileNameText(dbFile)
Spacer()
VStack(spacing: 10) {
saveAndOpenButton()
fileNameText(dbFile)
}
} else {
titleText("Encrypted database")
Text("Database passphrase is required to open chat.")
.font(.callout)
.foregroundColor(theme.colors.secondary)
.multilineTextAlignment(.center)
.padding(.horizontal, 25)
.padding(.bottom, 5)
if useKeychain {
databaseKeyField(onSubmit: saveAndRunChat)
Spacer()
saveAndOpenButton()
} else {
databaseKeyField(onSubmit: { runChat() })
Spacer()
openChatButton()
}
}
@@ -52,73 +69,105 @@ struct DatabaseErrorView: View {
switch migrationError {
case let .upgrade(upMigrations):
titleText("Database upgrade")
Button("Upgrade and open chat") { runChat(confirmMigrations: .yesUp) }
fileNameText(dbFile)
migrationsText(upMigrations.map(\.upName))
Spacer()
VStack(spacing: 10) {
Button("Upgrade and open chat") {
runChat(confirmMigrations: .yesUp)
}.buttonStyle(OnboardingButtonStyle(isDisabled: false))
fileNameText(dbFile)
}
case let .downgrade(downMigrations):
titleText("Database downgrade")
Text("Warning: you may lose some data!").bold()
Button("Downgrade and open chat") { runChat(confirmMigrations: .yesUpDown) }
fileNameText(dbFile)
Text("Warning: you may lose some data!")
.bold()
.padding(.horizontal, 25)
.multilineTextAlignment(.center)
migrationsText(downMigrations)
Spacer()
VStack(spacing: 10) {
Button("Downgrade and open chat") {
runChat(confirmMigrations: .yesUpDown)
}.buttonStyle(OnboardingButtonStyle(isDisabled: false))
fileNameText(dbFile)
}
case let .migrationError(mtrError):
titleText("Incompatible database version")
fileNameText(dbFile)
Text("Error: ") + Text(mtrErrorDescription(mtrError))
fileNameText(dbFile, font: .callout)
errorView(Text(mtrErrorDescription(mtrError)))
}
case let .errorSQL(dbFile, migrationSQLError):
titleText("Database error")
fileNameText(dbFile)
Text("Error: \(migrationSQLError)")
fileNameText(dbFile, font: .callout)
errorView(Text("Error: \(migrationSQLError)"))
case .errorKeychain:
titleText("Keychain error")
Text("Cannot access keychain to save database password")
errorView(Text("Cannot access keychain to save database password"))
case .invalidConfirmation:
// this can only happen if incorrect parameter is passed
Text(String("Invalid migration confirmation")).font(.title)
titleText("Invalid migration confirmation")
errorView()
case let .unknown(json):
titleText("Database error")
Text("Unknown database error: \(json)")
errorView(Text("Unknown database error: \(json)"))
case .ok:
EmptyView()
}
if showRestoreDbButton {
Spacer().frame(height: 10)
Spacer()
Text("The attempt to change database passphrase was not completed.")
.multilineTextAlignment(.center)
.padding(.horizontal, 25)
.font(.footnote)
restoreDbButton()
}
}
.padding()
.padding(.horizontal, 25)
.padding(.top, 75)
.padding(.bottom, 25)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
.onAppear() { showRestoreDbButton = shouldShowRestoreDbButton() }
}
private func titleText(_ s: LocalizedStringKey) -> Text {
Text(s).font(.title)
private func titleText(_ s: LocalizedStringKey) -> some View {
Text(s).font(.largeTitle).bold().multilineTextAlignment(.center)
}
private func fileNameText(_ f: String) -> Text {
Text("File: \((f as NSString).lastPathComponent)")
private func fileNameText(_ f: String, font: Font = .caption) -> Text {
Text("File: \((f as NSString).lastPathComponent)").font(font)
}
private func migrationsText(_ ms: [String]) -> Text {
Text("Migrations: \(ms.joined(separator: ", "))")
private func migrationsText(_ ms: [String]) -> some View {
(Text("Migrations:").font(.subheadline) + Text(verbatim: "\n") + Text(ms.joined(separator: "\n")).font(.caption))
.multilineTextAlignment(.center)
.padding(.horizontal, 25)
}
private func databaseKeyField(onSubmit: @escaping () -> Void) -> some View {
PassphraseField(key: $dbKey, placeholder: "Enter passphrase…", valid: validKey(dbKey), onSubmit: onSubmit)
.padding(.vertical, 10)
.padding(.horizontal)
.background(
RoundedRectangle(cornerRadius: 10, style: .continuous)
.fill(Color(uiColor: .tertiarySystemFill))
)
}
private func saveAndOpenButton() -> some View {
Button("Save passphrase and open chat") {
saveAndRunChat()
}
.buttonStyle(OnboardingButtonStyle(isDisabled: false))
}
private func openChatButton() -> some View {
Button("Open chat") {
runChat()
}
.buttonStyle(OnboardingButtonStyle(isDisabled: false))
}
private func saveAndRunChat() {
@@ -192,8 +241,9 @@ struct DatabaseErrorView: View {
secondaryButton: .cancel()
))
} label: {
Text("Restore database backup").foregroundColor(.red)
Text("Restore database backup")
}
.buttonStyle(OnboardingButtonStyle(isDisabled: false))
}
private func restoreDb() {
@@ -208,6 +258,23 @@ struct DatabaseErrorView: View {
))
}
}
private func errorView(_ s: Text? = nil) -> some View {
VStack(spacing: 35) {
Image(systemName: "exclamationmark.triangle.fill")
.resizable()
.frame(width: 50, height: 50)
.foregroundColor(.red)
if let text = s {
text
.multilineTextAlignment(.center)
.font(.footnote)
}
}
.padding()
.frame(maxWidth: .infinity)
}
}
struct DatabaseErrorView_Previews: PreviewProvider {
+234 -128
View File
@@ -44,8 +44,9 @@ enum DatabaseAlert: Identifiable {
struct DatabaseView: View {
@EnvironmentObject var m: ChatModel
@EnvironmentObject var theme: AppTheme
@Binding var showSettings: Bool
let dismissSettingsSheet: DismissAction
@State private var runChat = false
@State private var stoppingChat = false
@State private var alert: DatabaseAlert? = nil
@State private var showFileImporter = false
@State private var importedArchivePath: URL?
@@ -57,6 +58,8 @@ struct DatabaseView: View {
@State private var useKeychain = storeDBPassphraseGroupDefault.get()
@State private var appFilesCountAndSize: (Int, Int)?
@State private var showDatabaseEncryptionView = false
@State var chatItemTTL: ChatItemTTL
@State private var currentChatItemTTL: ChatItemTTL = .none
@@ -69,7 +72,20 @@ struct DatabaseView: View {
}
}
@ViewBuilder
private func chatDatabaseView() -> some View {
NavigationLink(isActive: $showDatabaseEncryptionView) {
DatabaseEncryptionView(useKeychain: $useKeychain, migration: false, stopChatRunBlockStartChat: { progressIndicator, block in
stopChatRunBlockStartChat(false, progressIndicator, block)
})
.navigationTitle("Database passphrase")
.modifier(ThemedBackground(grouped: true))
} label: {
EmptyView()
}
.frame(width: 1, height: 1)
.hidden()
List {
let stopped = m.chatRunning == false
Section {
@@ -101,9 +117,10 @@ struct DatabaseView: View {
isOn: $runChat
)
.onChange(of: runChat) { _ in
if (runChat) {
startChat()
} else {
if runChat {
DatabaseView.startChat($runChat, $progressIndicator)
} else if !stoppingChat {
stoppingChat = false
alert = .stopChat
}
}
@@ -123,7 +140,9 @@ struct DatabaseView: View {
let color: Color = unencrypted ? .orange : theme.colors.secondary
settingsRow(unencrypted ? "lock.open" : useKeychain ? "key" : "lock", color: color) {
NavigationLink {
DatabaseEncryptionView(useKeychain: $useKeychain, migration: false)
DatabaseEncryptionView(useKeychain: $useKeychain, migration: false, stopChatRunBlockStartChat: { progressIndicator, block in
stopChatRunBlockStartChat(false, progressIndicator, block)
})
.navigationTitle("Database passphrase")
.modifier(ThemedBackground(grouped: true))
} label: {
@@ -133,9 +152,14 @@ struct DatabaseView: View {
settingsRow("square.and.arrow.up", color: theme.colors.secondary) {
Button("Export database") {
if initialRandomDBPassphraseGroupDefault.get() && !unencrypted {
alert = .exportProhibited
showDatabaseEncryptionView = true
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
alert = .exportProhibited
}
} else {
exportArchive()
stopChatRunBlockStartChat(stopped, $progressIndicator) {
await exportArchive()
}
}
}
}
@@ -144,20 +168,6 @@ struct DatabaseView: View {
showFileImporter = true
}
}
if let archiveName = chatArchiveName {
let title: LocalizedStringKey = chatArchiveTimeDefault.get() < chatLastStartGroupDefault.get()
? "Old database archive"
: "New database archive"
settingsRow("archivebox", color: theme.colors.secondary) {
NavigationLink {
ChatArchiveView(archiveName: archiveName)
.navigationTitle(title)
.modifier(ThemedBackground(grouped: true))
} label: {
Text(title)
}
}
}
settingsRow("trash.slash", color: theme.colors.secondary) {
Button("Delete database", role: .destructive) {
alert = .deleteChat
@@ -167,14 +177,10 @@ struct DatabaseView: View {
Text("Chat database")
.foregroundColor(theme.colors.secondary)
} footer: {
Text(
stopped
? "You must use the most recent version of your chat database on one device ONLY, otherwise you may stop receiving the messages from some contacts."
: "Stop chat to enable database actions"
)
Text("You must use the most recent version of your chat database on one device ONLY, otherwise you may stop receiving the messages from some contacts.")
.foregroundColor(theme.colors.secondary)
}
.disabled(!stopped)
.disabled(progressIndicator)
if case .group = dbContainer, legacyDatabase {
Section(header: Text("Old database").foregroundColor(theme.colors.secondary)) {
@@ -190,7 +196,7 @@ struct DatabaseView: View {
Button(m.users.count > 1 ? "Delete files for all chat profiles" : "Delete all files", role: .destructive) {
alert = .deleteFilesAndMedia
}
.disabled(!stopped || appFilesCountAndSize?.0 == 0)
.disabled(progressIndicator || appFilesCountAndSize?.0 == 0)
} header: {
Text("Files & media")
.foregroundColor(theme.colors.secondary)
@@ -255,7 +261,10 @@ struct DatabaseView: View {
title: Text("Import chat database?"),
message: Text("Your current chat database will be DELETED and REPLACED with the imported one.") + Text("This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost."),
primaryButton: .destructive(Text("Import")) {
importArchive(fileURL)
stopChatRunBlockStartChat(m.chatRunning == false, $progressIndicator) {
_ = await DatabaseView.importArchive(fileURL, $progressIndicator, $alert)
return true
}
},
secondaryButton: .cancel()
)
@@ -263,19 +272,15 @@ struct DatabaseView: View {
return Alert(title: Text("Error: no database file"))
}
case .archiveImported:
return Alert(
title: Text("Chat database imported"),
message: Text("Restart the app to use imported chat database")
)
let (title, message) = archiveImportedAlertText()
return Alert(title: Text(title), message: Text(message))
case let .archiveImportedWithErrors(errs):
return Alert(
title: Text("Chat database imported"),
message: Text("Restart the app to use imported chat database") + Text(verbatim: "\n\n") + Text("Some non-fatal errors occurred during import:") + archiveErrorsText(errs)
)
let (title, message) = archiveImportedWithErrorsAlertText(errs: errs)
return Alert(title: Text(title), message: Text(message))
case let .archiveExportedWithErrors(archivePath, errs):
return Alert(
title: Text("Chat database exported"),
message: Text("You may save the exported archive.") + Text(verbatim: "\n\n") + Text("Some file(s) were not exported:") + archiveErrorsText(errs),
message: Text("You may save the exported archive.") + Text(verbatim: "\n") + Text("Some file(s) were not exported:") + Text(archiveErrorsText(errs)),
dismissButton: .default(Text("Continue")) {
showShareSheet(items: [archivePath])
}
@@ -285,15 +290,17 @@ struct DatabaseView: View {
title: Text("Delete chat profile?"),
message: Text("This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost."),
primaryButton: .destructive(Text("Delete")) {
deleteChat()
let wasStopped = m.chatRunning == false
stopChatRunBlockStartChat(wasStopped, $progressIndicator) {
_ = await deleteChat()
return true
}
},
secondaryButton: .cancel()
)
case .chatDeleted:
return Alert(
title: Text("Chat database deleted"),
message: Text("Restart the app to create a new chat profile")
)
let (title, message) = chatDeletedAlertText()
return Alert(title: Text(title), message: Text(message))
case .deleteLegacyDatabase:
return Alert(
title: Text("Delete old database?"),
@@ -308,7 +315,10 @@ struct DatabaseView: View {
title: Text("Delete files and media?"),
message: Text("This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain."),
primaryButton: .destructive(Text("Delete")) {
deleteFiles()
stopChatRunBlockStartChat(m.chatRunning == false, $progressIndicator) {
deleteFiles()
return true
}
},
secondaryButton: .cancel()
)
@@ -328,95 +338,180 @@ struct DatabaseView: View {
}
}
private func authStopChat() {
private func authStopChat(_ onStop: (() -> Void)? = nil) {
if UserDefaults.standard.bool(forKey: DEFAULT_PERFORM_LA) {
authenticate(reason: NSLocalizedString("Stop SimpleX", comment: "authentication reason")) { laResult in
switch laResult {
case .success: stopChat()
case .unavailable: stopChat()
case .success: stopChat(onStop)
case .unavailable: stopChat(onStop)
case .failed: withAnimation { runChat = true }
}
}
} else {
stopChat()
stopChat(onStop)
}
}
private func stopChat() {
private func stopChat(_ onStop: (() -> Void)? = nil) {
Task {
do {
try await stopChatAsync()
onStop?()
} catch let error {
await MainActor.run {
runChat = true
alert = .error(title: "Error stopping chat", error: responseError(error))
showAlert("Error stopping chat", message: responseError(error))
}
}
}
}
private func exportArchive() {
progressIndicator = true
Task {
do {
let (archivePath, archiveErrors) = try await exportChatArchive()
if archiveErrors.isEmpty {
showShareSheet(items: [archivePath])
await MainActor.run { progressIndicator = false }
} else {
await MainActor.run {
alert = .archiveExportedWithErrors(archivePath: archivePath, archiveErrors: archiveErrors)
progressIndicator = false
func stopChatRunBlockStartChat(
_ stopped: Bool,
_ progressIndicator: Binding<Bool>,
_ block: @escaping () async throws -> Bool
) {
// if the chat was running, the sequence is: stop chat, run block, start chat.
// Otherwise, just run block and do nothing - the toggle will be visible anyway and the user can start the chat or not
if stopped {
Task {
do {
_ = try await block()
} catch {
logger.error("Error while executing block: \(error)")
}
}
} else {
authStopChat {
stoppingChat = true
runChat = false
Task {
// if it throws, let's start chat again anyway
var canStart = false
do {
canStart = try await block()
} catch {
logger.error("Error executing block: \(error)")
canStart = true
}
if canStart {
await MainActor.run {
DatabaseView.startChat($runChat, $progressIndicator)
}
}
}
}
}
}
static func startChat(_ runChat: Binding<Bool>, _ progressIndicator: Binding<Bool>) {
progressIndicator.wrappedValue = true
let m = ChatModel.shared
if m.chatDbChanged {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
resetChatCtrl()
do {
let hadDatabase = hasDatabase()
try initializeChat(start: true)
m.chatDbChanged = false
AppChatState.shared.set(.active)
if m.chatDbStatus != .ok || !hadDatabase {
// Hide current view and show `DatabaseErrorView`
dismissAllSheets(animated: true)
}
} catch let error {
fatalError("Error starting chat \(responseError(error))")
}
progressIndicator.wrappedValue = false
}
} else {
do {
_ = try apiStartChat()
runChat.wrappedValue = true
m.chatRunning = true
ChatReceiver.shared.start()
chatLastStartGroupDefault.set(Date.now)
AppChatState.shared.set(.active)
} catch let error {
runChat.wrappedValue = false
showAlert(NSLocalizedString("Error starting chat", comment: ""), message: responseError(error))
}
progressIndicator.wrappedValue = false
}
}
private func exportArchive() async -> Bool {
await MainActor.run {
progressIndicator = true
}
do {
let (archivePath, archiveErrors) = try await exportChatArchive()
if archiveErrors.isEmpty {
showShareSheet(items: [archivePath])
await MainActor.run { progressIndicator = false }
} else {
await MainActor.run {
alert = .error(title: "Error exporting chat database", error: responseError(error))
alert = .archiveExportedWithErrors(archivePath: archivePath, archiveErrors: archiveErrors)
progressIndicator = false
}
}
} catch let error {
await MainActor.run {
alert = .error(title: "Error exporting chat database", error: responseError(error))
progressIndicator = false
}
}
return false
}
private func importArchive(_ archivePath: URL) {
static func importArchive(
_ archivePath: URL,
_ progressIndicator: Binding<Bool>,
_ alert: Binding<DatabaseAlert?>
) async -> Bool {
if archivePath.startAccessingSecurityScopedResource() {
progressIndicator = true
Task {
do {
try await apiDeleteStorage()
try? FileManager.default.createDirectory(at: getWallpaperDirectory(), withIntermediateDirectories: true)
do {
let config = ArchiveConfig(archivePath: archivePath.path)
let archiveErrors = try await apiImportArchive(config: config)
_ = kcDatabasePassword.remove()
if archiveErrors.isEmpty {
await operationEnded(.archiveImported)
} else {
await operationEnded(.archiveImportedWithErrors(archiveErrors: archiveErrors))
}
} catch let error {
await operationEnded(.error(title: "Error importing chat database", error: responseError(error)))
}
} catch let error {
await operationEnded(.error(title: "Error deleting chat database", error: responseError(error)))
}
archivePath.stopAccessingSecurityScopedResource()
await MainActor.run {
progressIndicator.wrappedValue = true
}
do {
try await apiDeleteStorage()
try? FileManager.default.createDirectory(at: getWallpaperDirectory(), withIntermediateDirectories: true)
do {
let config = ArchiveConfig(archivePath: archivePath.path)
let archiveErrors = try await apiImportArchive(config: config)
shouldImportAppSettingsDefault.set(true)
_ = kcDatabasePassword.remove()
if archiveErrors.isEmpty {
await operationEnded(.archiveImported, progressIndicator, alert)
} else {
await operationEnded(.archiveImportedWithErrors(archiveErrors: archiveErrors), progressIndicator, alert)
}
return true
} catch let error {
await operationEnded(.error(title: "Error importing chat database", error: responseError(error)), progressIndicator, alert)
}
} catch let error {
await operationEnded(.error(title: "Error deleting chat database", error: responseError(error)), progressIndicator, alert)
}
archivePath.stopAccessingSecurityScopedResource()
} else {
alert = .error(title: "Error accessing database file")
showAlert("Error accessing database file")
}
return false
}
private func deleteChat() {
progressIndicator = true
Task {
do {
try await deleteChatAsync()
await operationEnded(.chatDeleted)
appFilesCountAndSize = directoryFileCountAndSize(getAppFilesDirectory())
} catch let error {
await operationEnded(.error(title: "Error deleting database", error: responseError(error)))
}
private func deleteChat() async -> Bool {
await MainActor.run {
progressIndicator = true
}
do {
try await deleteChatAsync()
appFilesCountAndSize = directoryFileCountAndSize(getAppFilesDirectory())
await DatabaseView.operationEnded(.chatDeleted, $progressIndicator, $alert)
return true
} catch let error {
await DatabaseView.operationEnded(.error(title: "Error deleting database", error: responseError(error)), $progressIndicator, $alert)
return false
}
}
@@ -428,39 +523,28 @@ struct DatabaseView: View {
}
}
private func operationEnded(_ dbAlert: DatabaseAlert) async {
private static func operationEnded(_ dbAlert: DatabaseAlert, _ progressIndicator: Binding<Bool>, _ alert: Binding<DatabaseAlert?>) async {
await MainActor.run {
let m = ChatModel.shared
m.chatDbChanged = true
m.chatInitialized = false
progressIndicator = false
alert = dbAlert
progressIndicator.wrappedValue = false
}
}
private func startChat() {
if m.chatDbChanged {
showSettings = false
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
resetChatCtrl()
do {
try initializeChat(start: true)
m.chatDbChanged = false
AppChatState.shared.set(.active)
} catch let error {
fatalError("Error starting chat \(responseError(error))")
}
}
} else {
do {
_ = try apiStartChat()
runChat = true
m.chatRunning = true
ChatReceiver.shared.start()
chatLastStartGroupDefault.set(Date.now)
AppChatState.shared.set(.active)
} catch let error {
runChat = false
alert = .error(title: "Error starting chat", error: responseError(error))
await withCheckedContinuation { cont in
let okAlertActionWaiting = UIAlertAction(title: NSLocalizedString("Ok", comment: "alert button"), style: .default, handler: { _ in cont.resume() })
// show these alerts globally so they are visible when all sheets will be hidden
if case .archiveImported = dbAlert {
let (title, message) = archiveImportedAlertText()
showAlert(title, message: message, actions: { [okAlertActionWaiting] })
} else if case .archiveImportedWithErrors(let errs) = dbAlert {
let (title, message) = archiveImportedWithErrorsAlertText(errs: errs)
showAlert(title, message: message, actions: { [okAlertActionWaiting] })
} else if case .chatDeleted = dbAlert {
let (title, message) = chatDeletedAlertText()
showAlert(title, message: message, actions: { [okAlertActionWaiting] })
} else {
alert.wrappedValue = dbAlert
cont.resume()
}
}
}
@@ -503,8 +587,28 @@ struct DatabaseView: View {
}
}
func archiveErrorsText(_ errs: [ArchiveError]) -> Text {
return Text("\n" + errs.map(showArchiveError).joined(separator: "\n"))
private func archiveImportedAlertText() -> (String, String) {
(
NSLocalizedString("Chat database imported", comment: ""),
NSLocalizedString("Restart the app to use imported chat database", comment: "")
)
}
private func archiveImportedWithErrorsAlertText(errs: [ArchiveError]) -> (String, String) {
(
NSLocalizedString("Chat database imported", comment: ""),
NSLocalizedString("Restart the app to use imported chat database", comment: "") + "\n" + NSLocalizedString("Some non-fatal errors occurred during import:", comment: "") + archiveErrorsText(errs)
)
}
private func chatDeletedAlertText() -> (String, String) {
(
NSLocalizedString("Chat database deleted", comment: ""),
NSLocalizedString("Restart the app to create a new chat profile", comment: "")
)
}
func archiveErrorsText(_ errs: [ArchiveError]) -> String {
return "\n" + errs.map(showArchiveError).joined(separator: "\n")
func showArchiveError(_ err: ArchiveError) -> String {
switch err {
@@ -533,7 +637,9 @@ func deleteChatAsync() async throws {
}
struct DatabaseView_Previews: PreviewProvider {
@Environment(\.dismiss) static var mockDismiss
static var previews: some View {
DatabaseView(showSettings: Binding.constant(false), chatItemTTL: .none)
DatabaseView(dismissSettingsSheet: mockDismiss, chatItemTTL: .none)
}
}
@@ -117,7 +117,7 @@ struct MigrateToAppGroupView: View {
setV3DBMigration(.migration_error)
migrationError = "Error starting chat: \(responseError(error))"
}
deleteOldArchive()
deleteOldChatArchive()
} label: {
Text("Start chat")
.font(.title)
@@ -235,14 +235,16 @@ func exportChatArchive(_ storagePath: URL? = nil) async throws -> (URL, [Archive
try? FileManager.default.createDirectory(at: getWallpaperDirectory(), withIntermediateDirectories: true)
let errs = try await apiExportArchive(config: config)
if storagePath == nil {
deleteOldArchive()
deleteOldChatArchive()
UserDefaults.standard.set(archiveName, forKey: DEFAULT_CHAT_ARCHIVE_NAME)
chatArchiveTimeDefault.set(archiveTime)
}
return (archivePath, errs)
}
func deleteOldArchive() {
/// Deprecated. Remove in the end of 2025. All unused archives should be deleted for the most users til then.
/// Remove DEFAULT_CHAT_ARCHIVE_NAME and DEFAULT_CHAT_ARCHIVE_TIME as well
func deleteOldChatArchive() {
let d = UserDefaults.standard
if let archiveName = d.string(forKey: DEFAULT_CHAT_ARCHIVE_NAME) {
do {
+7 -5
View File
@@ -11,6 +11,12 @@ import SwiftUI
class AppSheetState: ObservableObject {
static let shared = AppSheetState()
@Published var scenePhaseActive: Bool = false
func redactionReasons(_ protectScreen: Bool) -> RedactionReasons {
!protectScreen || scenePhaseActive
? RedactionReasons()
: RedactionReasons.placeholder
}
}
private struct PrivacySensitive: ViewModifier {
@@ -19,11 +25,7 @@ private struct PrivacySensitive: ViewModifier {
@ObservedObject var appSheetState: AppSheetState = AppSheetState.shared
func body(content: Content) -> some View {
if !protectScreen {
content
} else {
content.privacySensitive(!appSheetState.scenePhaseActive).redacted(reason: .privacy)
}
content.redacted(reason: appSheetState.redactionReasons(protectScreen))
}
}
@@ -36,12 +36,7 @@ struct ChatItemClipped: ViewModifier {
.sndMsgContent,
.rcvMsgContent,
.rcvDecryptionError,
.sndDeleted,
.rcvDeleted,
.rcvIntegrityError,
.sndModerated,
.rcvModerated,
.rcvBlocked,
.invalidJSON:
let tail = if let mc = ci.content.msgContent, mc.isImageOrVideo && mc.text.isEmpty {
false
@@ -0,0 +1,21 @@
//
// Test.swift
// SimpleX (iOS)
//
// Created by Levitating Pineapple on 31/08/2024.
// Copyright © 2024 SimpleX Chat. All rights reserved.
//
import SwiftUI
extension View {
@ViewBuilder
func invertedForegroundStyle(enabled: Bool = true) -> some View {
if enabled {
foregroundStyle(Material.ultraThin)
.environment(\.colorScheme, .dark)
.grayscale(1)
.contrast(-20)
} else { self }
}
}
@@ -20,7 +20,7 @@ struct ProfileImage: View {
@AppStorage(DEFAULT_PROFILE_IMAGE_CORNER_RADIUS) private var radius = defaultProfileImageCorner
var body: some View {
if let uiImage = UIImage(base64Encoded: imageStr) {
if let uiImage = imageFromBase64(imageStr) {
clipProfileImage(Image(uiImage: uiImage), size: size, radius: radius, blurred: blurred)
} else {
let c = color.asAnotherColorFromSecondaryVariant(theme)
+54 -6
View File
@@ -8,15 +8,63 @@
import SwiftUI
func showShareSheet(items: [Any], completed: (() -> Void)? = nil) {
func getTopViewController() -> UIViewController? {
let keyWindowScene = UIApplication.shared.connectedScenes.first { $0.activationState == .foregroundActive } as? UIWindowScene
if let keyWindow = keyWindowScene?.windows.filter(\.isKeyWindow).first,
let presentedViewController = keyWindow.rootViewController?.presentedViewController ?? keyWindow.rootViewController {
let rootViewController = keyWindow.rootViewController {
// Find the top-most presented view controller
var topController = rootViewController
while let presentedViewController = topController.presentedViewController {
topController = presentedViewController
}
return topController
}
return nil
}
func showShareSheet(items: [Any], completed: (() -> Void)? = nil) {
if let topController = getTopViewController() {
let activityViewController = UIActivityViewController(activityItems: items, applicationActivities: nil)
if let completed = completed {
let handler: UIActivityViewController.CompletionWithItemsHandler = { _,_,_,_ in completed() }
activityViewController.completionWithItemsHandler = handler
}
presentedViewController.present(activityViewController, animated: true)
activityViewController.completionWithItemsHandler = { _, _, _, _ in
completed()
}
}
topController.present(activityViewController, animated: true)
}
}
func showAlert(
title: String,
message: String? = nil,
buttonTitle: String,
buttonAction: @escaping () -> Void,
cancelButton: Bool
) -> Void {
if let topController = getTopViewController() {
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: buttonTitle, style: .default) { _ in
buttonAction()
})
if cancelButton {
alert.addAction(cancelAlertAction)
}
topController.present(alert, animated: true)
}
}
func showAlert(
_ title: String,
message: String? = nil,
actions: () -> [UIAlertAction] = { [okAlertAction] }
) {
if let topController = getTopViewController() {
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
for action in actions() { alert.addAction(action) }
topController.present(alert, animated: true)
}
}
let okAlertAction = UIAlertAction(title: NSLocalizedString("Ok", comment: "alert button"), style: .default)
let cancelAlertAction = UIAlertAction(title: NSLocalizedString("Cancel", comment: "alert button"), style: .cancel)
@@ -0,0 +1,188 @@
//
// SwiftUISheet.swift
// SimpleX (iOS)
//
// Created by user on 23/09/2024.
// Copyright © 2024 SimpleX Chat. All rights reserved.
//
import SwiftUI
private let sheetAnimationDuration: Double = 0.35
// Refrence: https://easings.net/
private let easeOutCubic = UICubicTimingParameters(
controlPoint1: CGPoint(x: 0.215, y: 0.61),
controlPoint2: CGPoint(x: 0.355, y: 1)
)
struct Sheet<SheetContent: View>: ViewModifier {
@Binding var isPresented: Bool
@ViewBuilder let sheetContent: () -> SheetContent
func body(content: Content) -> some View {
ZStack {
content
SheetRepresentable(isPresented: $isPresented, content: sheetContent())
.allowsHitTesting(isPresented)
.ignoresSafeArea()
}
}
}
struct SheetRepresentable<Content: View>: UIViewControllerRepresentable {
@Binding var isPresented: Bool
let content: Content
func makeUIViewController(context: Context) -> Controller<Content> {
Controller(content: content, representer: self)
}
func updateUIViewController(_ sheetController: Controller<Content>, context: Context) {
sheetController.animate(isPresented: isPresented)
}
class Controller<C: View>: UIViewController {
let hostingController: UIHostingController<C>
private let animator = UIViewPropertyAnimator(
duration: sheetAnimationDuration,
timingParameters: easeOutCubic
)
private let representer: SheetRepresentable<C>
private var retainedFraction: CGFloat = 0
private var sheetHeight: Double { hostingController.view.frame.height }
private var task: Task<Void, Never>?
init(content: C, representer: SheetRepresentable<C>) {
self.representer = representer
self.hostingController = UIHostingController(rootView: content)
super.init(nibName: nil, bundle: nil)
}
@available(*, unavailable)
required init?(coder: NSCoder) { fatalError("init(coder:) missing") }
deinit {
animator.stopAnimation(true)
animator.finishAnimation(at: .current)
}
func animate(isPresented: Bool) {
let alreadyAnimating = animator.isRunning && isPresented != animator.isReversed
let sheetFullyDismissed = animator.fractionComplete == (animator.isReversed ? 1 : 0)
let sheetFullyPresented = animator.fractionComplete == (animator.isReversed ? 0 : 1)
if !isPresented && sheetFullyDismissed ||
isPresented && sheetFullyPresented ||
alreadyAnimating {
return
}
animator.pauseAnimation()
animator.isReversed = !isPresented
animator.continueAnimation(
withTimingParameters: isPresented
? easeOutCubic
: UICubicTimingParameters(animationCurve: .easeIn),
durationFactor: 1 - animator.fractionComplete
)
handleVisibility()
}
func handleVisibility() {
if animator.isReversed {
task = Task {
do {
let sleepDuration = UInt64(sheetAnimationDuration * Double(NSEC_PER_SEC))
try await Task.sleep(nanoseconds: sleepDuration)
view.isHidden = true
} catch { }
}
} else {
task?.cancel()
task = nil
view.isHidden = false
}
}
override func viewDidLoad() {
view.isHidden = true
view.backgroundColor = .clear
view.addGestureRecognizer(
UITapGestureRecognizer(target: self, action: #selector(tap(gesture:)))
)
addChild(hostingController)
hostingController.didMove(toParent: self)
if let sheet = hostingController.view {
sheet.isHidden = true
sheet.clipsToBounds = true
sheet.layer.cornerRadius = 10
sheet.layer.maskedCorners = [.layerMaxXMinYCorner, .layerMinXMinYCorner]
sheet.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(pan(gesture:))))
sheet.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(sheet)
NSLayoutConstraint.activate([
hostingController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
hostingController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
hostingController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
])
}
}
override func viewDidAppear(_ animated: Bool) {
// Ensures animations are only setup once
// on some iOS version `viewDidAppear` can get called on each state change.
if hostingController.view.isHidden {
hostingController.view.transform = CGAffineTransform(translationX: 0, y: self.sheetHeight)
hostingController.view.isHidden = false
animator.pausesOnCompletion = true
animator.addAnimations {
self.hostingController.view.transform = .identity
self.view.backgroundColor = UIColor {
switch $0.userInterfaceStyle {
case .dark: .black.withAlphaComponent(0.290)
default: .black.withAlphaComponent(0.121)
}
}
}
animator.startAnimation()
animator.pauseAnimation()
}
}
@objc
func pan(gesture: UIPanGestureRecognizer) {
switch gesture.state {
case .began:
animator.isReversed = false
animator.pauseAnimation()
retainedFraction = animator.fractionComplete
case .changed:
animator.fractionComplete = retainedFraction - gesture.translation(in: view).y / sheetHeight
case .ended, .cancelled:
let velocity = gesture.velocity(in: view).y
animator.isReversed = (velocity - (animator.fractionComplete - 0.5) * 100).sign == .plus
let defaultVelocity = sheetHeight / sheetAnimationDuration
let fractionRemaining = 1 - animator.fractionComplete
let durationFactor = min(max(fractionRemaining / (abs(velocity) / defaultVelocity), 0.5), 1)
animator.continueAnimation(withTimingParameters: nil, durationFactor: durationFactor * fractionRemaining)
handleVisibility()
DispatchQueue.main.asyncAfter(deadline: .now() + sheetAnimationDuration) {
self.representer.isPresented = !self.animator.isReversed
}
default: break
}
}
@objc
func tap(gesture: UITapGestureRecognizer) {
switch gesture.state {
case .ended:
if gesture.location(in: view).y < view.frame.height - sheetHeight {
representer.isPresented = false
}
default: break
}
}
}
}
@@ -0,0 +1,61 @@
//
// StickyScrollView.swift
// SimpleX (iOS)
//
// Created by user on 20/09/2024.
// Copyright © 2024 SimpleX Chat. All rights reserved.
//
import SwiftUI
struct StickyScrollView<Content: View>: UIViewRepresentable {
@Binding var resetScroll: ResetScrollAction
@ViewBuilder let content: () -> Content
func makeUIView(context: Context) -> UIScrollView {
let hc = context.coordinator.hostingController
hc.view.backgroundColor = .clear
let sv = UIScrollView()
sv.showsHorizontalScrollIndicator = false
sv.addSubview(hc.view)
sv.delegate = context.coordinator
DispatchQueue.main.async {
resetScroll = ResetScrollAction { sv.setContentOffset(.zero, animated: false) }
}
return sv
}
func updateUIView(_ scrollView: UIScrollView, context: Context) {
let hc = context.coordinator.hostingController
hc.rootView = content()
hc.view.frame.size = hc.view.intrinsicContentSize
scrollView.contentSize = hc.view.intrinsicContentSize
}
func makeCoordinator() -> Coordinator {
Coordinator(content: content())
}
class Coordinator: NSObject, UIScrollViewDelegate {
let hostingController: UIHostingController<Content>
init(content: Content) {
self.hostingController = UIHostingController(rootView: content)
}
func scrollViewWillEndDragging(
_ scrollView: UIScrollView,
withVelocity velocity: CGPoint,
targetContentOffset: UnsafeMutablePointer<CGPoint>
) {
if targetContentOffset.pointee.x < 32 {
targetContentOffset.pointee.x = 0
}
}
}
}
struct ResetScrollAction {
var action = { }
func callAsFunction() { action() }
}
@@ -16,6 +16,7 @@ struct UserWallpaperEditor: View {
@State var themeModeOverride: ThemeModeOverride
@State var applyToMode: DefaultThemeMode?
@State var showMore: Bool = false
@State var showFileImporter: Bool = false
@Binding var globalThemeUsed: Bool
var save: (DefaultThemeMode?, ThemeModeOverride?) async -> Void
@@ -125,24 +126,27 @@ struct UserWallpaperEditor: View {
CustomizeThemeColorsSection(editColor: { name in editColor(name, theme) })
ImportExportThemeSection(perChat: nil, perUser: ChatModel.shared.currentUser?.uiThemes) { imported in
let importedFromString = imported.wallpaper?.importFromString()
let importedType = importedFromString?.toAppWallpaper().type
let currentTheme = ThemeManager.currentColors(nil, nil, nil, themeOverridesDefault.get())
let type: WallpaperType? = if importedType?.sameType(currentTheme.wallpaper.type) == true { nil } else { importedType }
let colors = ThemeManager.currentThemeOverridesForExport(type, nil, nil).colors
let res = ThemeModeOverride(mode: imported.base.mode, colors: imported.colors, wallpaper: importedFromString).removeSameColors(imported.base, colorsToCompare: colors)
Task {
await MainActor.run {
themeModeOverride = res
}
await save(applyToMode, res)
}
}
ImportExportThemeSection(showFileImporter: $showFileImporter, perChat: nil, perUser: ChatModel.shared.currentUser?.uiThemes)
} else {
AdvancedSettingsButton(theme.colors.primary) { showMore = true }
}
}
.modifier(
ThemeImporter(isPresented: $showFileImporter) { imported in
let importedFromString = imported.wallpaper?.importFromString()
let importedType = importedFromString?.toAppWallpaper().type
let currentTheme = ThemeManager.currentColors(nil, nil, nil, themeOverridesDefault.get())
let type: WallpaperType? = if importedType?.sameType(currentTheme.wallpaper.type) == true { nil } else { importedType }
let colors = ThemeManager.currentThemeOverridesForExport(type, nil, nil).colors
let res = ThemeModeOverride(mode: imported.base.mode, colors: imported.colors, wallpaper: importedFromString).removeSameColors(imported.base, colorsToCompare: colors)
Task {
await MainActor.run {
themeModeOverride = res
}
await save(applyToMode, res)
}
}
)
}
private func onTypeCopyFromSameTheme(_ type: WallpaperType?) -> Bool {
@@ -216,6 +220,7 @@ struct ChatWallpaperEditor: View {
@State var themeModeOverride: ThemeModeOverride
@State var applyToMode: DefaultThemeMode?
@State var showMore: Bool = false
@State var showFileImporter: Bool = false
@Binding var globalThemeUsed: Bool
var save: (DefaultThemeMode?, ThemeModeOverride?) async -> Void
@@ -328,24 +333,27 @@ struct ChatWallpaperEditor: View {
CustomizeThemeColorsSection(editColor: editColor)
ImportExportThemeSection(perChat: themeModeOverride, perUser: ChatModel.shared.currentUser?.uiThemes) { imported in
let importedFromString = imported.wallpaper?.importFromString()
let importedType = importedFromString?.toAppWallpaper().type
let currentTheme = ThemeManager.currentColors(nil, nil, ChatModel.shared.currentUser?.uiThemes, themeOverridesDefault.get())
let type: WallpaperType? = if importedType?.sameType(currentTheme.wallpaper.type) == true { nil } else { importedType }
let colors = ThemeManager.currentThemeOverridesForExport(type, nil, ChatModel.shared.currentUser?.uiThemes).colors
let res = ThemeModeOverride(mode: imported.base.mode, colors: imported.colors, wallpaper: importedFromString).removeSameColors(imported.base, colorsToCompare: colors)
Task {
await MainActor.run {
themeModeOverride = res
}
await save(applyToMode, res)
}
}
ImportExportThemeSection(showFileImporter: $showFileImporter, perChat: themeModeOverride, perUser: ChatModel.shared.currentUser?.uiThemes)
} else {
AdvancedSettingsButton(theme.colors.primary) { showMore = true }
}
}
.modifier(
ThemeImporter(isPresented: $showFileImporter) { imported in
let importedFromString = imported.wallpaper?.importFromString()
let importedType = importedFromString?.toAppWallpaper().type
let currentTheme = ThemeManager.currentColors(nil, nil, ChatModel.shared.currentUser?.uiThemes, themeOverridesDefault.get())
let type: WallpaperType? = if importedType?.sameType(currentTheme.wallpaper.type) == true { nil } else { importedType }
let colors = ThemeManager.currentThemeOverridesForExport(type, nil, ChatModel.shared.currentUser?.uiThemes).colors
let res = ThemeModeOverride(mode: imported.base.mode, colors: imported.colors, wallpaper: importedFromString).removeSameColors(imported.base, colorsToCompare: colors)
Task {
await MainActor.run {
themeModeOverride = res
}
await save(applyToMode, res)
}
}
)
}
private func onTypeCopyFromSameTheme(_ type: WallpaperType?) -> Bool {
@@ -0,0 +1,62 @@
//
// UserDefault.swift
// SimpleX (iOS)
//
// Created by user on 14/10/2024.
// Copyright © 2024 SimpleX Chat. All rights reserved.
//
import SwiftUI
import Combine
@propertyWrapper
public struct UserDefault<Value: Equatable>: DynamicProperty {
@StateObject private var observer = UserDefaultObserver()
let initialValue: Value
let key: String
let store: UserDefaults
public init(
wrappedValue: Value,
_ key: String,
store: UserDefaults = .standard
) {
self.initialValue = wrappedValue
self.key = key
self.store = store
}
public var wrappedValue: Value {
get {
// Observer can only be accessed after the property wrapper is installed in view (runtime exception)
observer.subscribe(to: key)
return store.object(forKey: key) as? Value ?? initialValue
}
nonmutating set {
store.set(newValue, forKey: key)
}
}
}
private class UserDefaultObserver: ObservableObject {
private var subscribed = false
func subscribe(to key: String) {
if !subscribed {
NotificationCenter.default.addObserver(
self,
selector: #selector(userDefaultsDidChange),
name: UserDefaults.didChangeNotification,
object: nil
)
subscribed = true
}
}
@objc
private func userDefaultsDidChange(_ notification: Notification) {
Task { @MainActor in objectWillChange.send() }
}
deinit { NotificationCenter.default.removeObserver(self) }
}
@@ -24,6 +24,7 @@ private enum MigrationFromState: Equatable {
}
private enum MigrateFromDeviceViewAlert: Identifiable {
case finishMigration(_ fileId: Int64, _ ctrl: chat_ctrl)
case deleteChat(_ title: LocalizedStringKey = "Delete chat profile?", _ text: LocalizedStringKey = "This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost.")
case startChat(_ title: LocalizedStringKey = "Start chat?", _ text: LocalizedStringKey = "Warning: starting chat on multiple devices is not supported and will cause message delivery failures")
@@ -38,6 +39,7 @@ private enum MigrateFromDeviceViewAlert: Identifiable {
var id: String {
switch self {
case .finishMigration: return "finishMigration"
case let .deleteChat(title, text): return "\(title) \(text)"
case let .startChat(title, text): return "\(title) \(text)"
@@ -56,8 +58,6 @@ private enum MigrateFromDeviceViewAlert: Identifiable {
struct MigrateFromDevice: View {
@EnvironmentObject var m: ChatModel
@EnvironmentObject var theme: AppTheme
@Environment(\.dismiss) var dismiss: DismissAction
@Binding var showSettings: Bool
@Binding var showProgressOnSettings: Bool
@State private var migrationState: MigrationFromState = .chatStopInProgress
@State private var useKeychain = storeDBPassphraseGroupDefault.get()
@@ -106,9 +106,6 @@ struct MigrateFromDevice: View {
finishedView(chatDeletion)
}
}
.modifier(BackButton(label: "Back", disabled: $backDisabled) {
dismiss()
})
.onChange(of: migrationState) { state in
backDisabled = switch migrationState {
case .chatStopInProgress, .archiving, .linkShown, .finished: true
@@ -138,6 +135,15 @@ struct MigrateFromDevice: View {
}
.alert(item: $alert) { alert in
switch alert {
case let .finishMigration(fileId, ctrl):
return Alert(
title: Text("Remove archive?"),
message: Text("The uploaded database archive will be permanently removed from the servers."),
primaryButton: .destructive(Text("Continue")) {
finishMigration(fileId, ctrl)
},
secondaryButton: .cancel()
)
case let .startChat(title, text):
return Alert(
title: Text(title),
@@ -171,7 +177,7 @@ struct MigrateFromDevice: View {
case let .archiveExportedWithErrors(archivePath, errs):
return Alert(
title: Text("Chat database exported"),
message: Text("You may migrate the exported database.") + Text(verbatim: "\n\n") + Text("Some file(s) were not exported:") + archiveErrorsText(errs),
message: Text("You may migrate the exported database.") + Text(verbatim: "\n") + Text("Some file(s) were not exported:") + Text(archiveErrorsText(errs)),
dismissButton: .default(Text("Continue")) {
Task { await uploadArchive(path: archivePath) }
}
@@ -216,7 +222,8 @@ struct MigrateFromDevice: View {
}
private func passphraseNotSetView() -> some View {
DatabaseEncryptionView(useKeychain: $useKeychain, migration: true)
DatabaseEncryptionView(useKeychain: $useKeychain, migration: true, stopChatRunBlockStartChat: { _, _ in
})
.onChange(of: initialRandomDBPassphrase) { initial in
if !initial {
migrationState = .uploadConfirmation
@@ -318,7 +325,7 @@ struct MigrateFromDevice: View {
Text("Cancel migration").foregroundColor(.red)
}
}
Button(action: { finishMigration(fileId, ctrl) }) {
Button(action: { alert = .finishMigration(fileId, ctrl) }) {
settingsRow("checkmark", color: theme.colors.secondary) {
Text("Finalize migration").foregroundColor(theme.colors.primary)
}
@@ -523,9 +530,15 @@ struct MigrateFromDevice: View {
}
case let .sndStandaloneFileComplete(_, fileTransferMeta, rcvURIs):
let cfg = getNetCfg()
let proxy: NetworkProxy? = if cfg.socksProxy == nil {
nil
} else {
networkProxyDefault.get()
}
let data = MigrationFileLinkData.init(
networkConfig: MigrationFileLinkData.NetworkConfig(
socksProxy: cfg.socksProxy,
networkProxy: proxy,
hostMode: cfg.hostMode,
requiredHostMode: cfg.requiredHostMode
)
@@ -590,7 +603,7 @@ struct MigrateFromDevice: View {
} catch let error {
fatalError("Error starting chat \(responseError(error))")
}
showSettings = false
dismissAllSheets(animated: true)
}
} catch let error {
alert = .error(title: "Error deleting database", error: responseError(error))
@@ -613,9 +626,7 @@ struct MigrateFromDevice: View {
}
// Hide settings anyway if chatDbStatus is not ok, probably passphrase needs to be entered
if dismiss || m.chatDbStatus != .ok {
await MainActor.run {
showSettings = false
}
dismissAllSheets(animated: true)
}
}
@@ -767,6 +778,6 @@ private class MigrationChatReceiver {
struct MigrateFromDevice_Previews: PreviewProvider {
static var previews: some View {
MigrateFromDevice(showSettings: Binding.constant(true), showProgressOnSettings: Binding.constant(false))
MigrateFromDevice(showProgressOnSettings: Binding.constant(false))
}
}
@@ -93,7 +93,6 @@ struct MigrateToDevice: View {
@EnvironmentObject var m: ChatModel
@EnvironmentObject var theme: AppTheme
@Environment(\.dismiss) var dismiss: DismissAction
@AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false
@Binding var migrationState: MigrationToState?
@State private var useKeychain = storeDBPassphraseGroupDefault.get()
@State private var alert: MigrateToDeviceViewAlert?
@@ -102,6 +101,10 @@ struct MigrateToDevice: View {
// Prevent from hiding the view until migration is finished or app deleted
@State private var backDisabled: Bool = false
@State private var showQRCodeScanner: Bool = true
@State private var pasteboardHasStrings = UIPasteboard.general.hasStrings
@State private var importingArchiveFromFileProgressIndicator = false
@State private var showFileImporter = false
var body: some View {
VStack {
@@ -197,11 +200,15 @@ struct MigrateToDevice: View {
}
}
}
if developerTools {
Section(header: Text("Or paste archive link").foregroundColor(theme.colors.secondary)) {
pasteLinkView()
}
Section(header: Text("Or paste archive link").foregroundColor(theme.colors.secondary)) {
pasteLinkView()
}
Section(header: Text("Or import archive file").foregroundColor(theme.colors.secondary)) {
archiveImportFromFileView()
}
}
if importingArchiveFromFileProgressIndicator {
progressView()
}
}
}
@@ -218,10 +225,38 @@ struct MigrateToDevice: View {
} label: {
Text("Tap to paste link")
}
.disabled(!ChatModel.shared.pasteboardHasStrings)
.disabled(!pasteboardHasStrings)
.frame(maxWidth: .infinity, alignment: .center)
}
private func archiveImportFromFileView() -> some View {
Button {
showFileImporter = true
} label: {
Label("Import database", systemImage: "square.and.arrow.down")
}
.disabled(importingArchiveFromFileProgressIndicator)
.fileImporter(
isPresented: $showFileImporter,
allowedContentTypes: [.zip],
allowsMultipleSelection: false
) { result in
if case let .success(files) = result, let fileURL = files.first {
Task {
let success = await DatabaseView.importArchive(fileURL, $importingArchiveFromFileProgressIndicator, Binding.constant(nil))
if success {
DatabaseView.startChat(
Binding.constant(false),
$importingArchiveFromFileProgressIndicator
)
hideView()
}
}
}
}
}
private func linkDownloadingView(_ link: String) -> some View {
ZStack {
List {
@@ -487,6 +522,9 @@ struct MigrateToDevice: View {
do {
if !hasChatCtrl() {
chatInitControllerRemovingDatabases()
} else if ChatModel.shared.chatRunning == true {
// cannot delete storage if chat is running
try await apiStopChat()
}
try await apiDeleteStorage()
try? FileManager.default.createDirectory(at: getWallpaperDirectory(), withIntermediateDirectories: true)
@@ -556,11 +594,22 @@ struct MigrateToDevice: View {
do {
try? FileManager.default.removeItem(at: getMigrationTempFilesDirectory())
MigrationToDeviceState.save(nil)
appSettings.importIntoApp()
try SimpleX.startChat(refreshInvitations: true)
AlertManager.shared.showAlertMsg(title: "Chat migrated!", message: "Finalize migration on another device.")
try ObjC.catchException {
appSettings.importIntoApp()
}
do {
try SimpleX.startChat(refreshInvitations: true)
AlertManager.shared.showAlertMsg(title: "Chat migrated!", message: "Finalize migration on another device.")
} catch let error {
AlertManager.shared.showAlert(Alert(title: Text("Error starting chat"), message: Text(responseError(error))))
}
} catch let error {
AlertManager.shared.showAlert(Alert(title: Text("Error starting chat"), message: Text(responseError(error))))
logger.error("Error importing settings: \(error.localizedDescription)")
AlertManager.shared.showAlert(
Alert(
title: Text("Error migrating settings"),
message: Text ("Some app settings were not migrated.") + Text("\n") + Text(responseError(error)))
)
}
hideView()
}
@@ -137,10 +137,13 @@ struct AddGroupView: View {
createInvalidNameAlert(mkValidName(profile.displayName), $profile.displayName)
}
.onChange(of: chosenImage) { image in
if let image = image {
profile.image = resizeImageToStrSize(cropToSquare(image), maxDataSize: 12500)
} else {
profile.image = nil
Task {
let resized: String? = if let image {
await resizeImageToStrSize(cropToSquare(image), maxDataSize: 12500)
} else {
nil
}
await MainActor.run { profile.image = resized }
}
}
.modifier(ThemedBackground(grouped: true))
@@ -14,10 +14,10 @@ enum ContactType: Int {
}
struct NewChatMenuButton: View {
@EnvironmentObject var chatModel: ChatModel
// do not use chatModel here because it prevents showing AddGroupMembersView after group creation and QR code after link creation on iOS 16
// @EnvironmentObject var chatModel: ChatModel
@State private var showNewChatSheet = false
@State private var alert: SomeAlert? = nil
@State private var pendingConnection: PendingContactConnection? = nil
var body: some View {
Button {
@@ -29,12 +29,8 @@ struct NewChatMenuButton: View {
.frame(width: 24, height: 24)
}
.appSheet(isPresented: $showNewChatSheet) {
NewChatSheet(pendingConnection: $pendingConnection)
NewChatSheet()
.environment(\EnvironmentValues.refresh as! WritableKeyPath<EnvironmentValues, RefreshAction?>, nil)
.onDisappear {
alert = cleanupPendingConnection(chatModel: chatModel, contactConnection: pendingConnection)
pendingConnection = nil
}
}
.alert(item: $alert) { a in
return a.alert
@@ -54,7 +50,6 @@ struct NewChatSheet: View {
@State private var searchShowingSimplexLink = false
@State private var searchChatFilteredBySimplexLink: String? = nil
@State private var alert: SomeAlert?
@Binding var pendingConnection: PendingContactConnection?
// Sheet height management
@State private var isAddContactActive = false
@@ -109,17 +104,17 @@ struct NewChatSheet: View {
if (searchText.isEmpty) {
Section {
NavigationLink(isActive: $isAddContactActive) {
NewChatView(selection: .invite, parentAlert: $alert, contactConnection: $pendingConnection)
NewChatView(selection: .invite)
.navigationTitle("New chat")
.modifier(ThemedBackground(grouped: true))
.navigationBarTitleDisplayMode(.large)
} label: {
navigateOnTap(Label("Add contact", systemImage: "link.badge.plus")) {
navigateOnTap(Label("Create 1-time link", systemImage: "link.badge.plus")) {
isAddContactActive = true
}
}
NavigationLink(isActive: $isScanPasteLinkActive) {
NewChatView(selection: .connect, showQRCodeScanner: true, parentAlert: $alert, contactConnection: $pendingConnection)
NewChatView(selection: .connect, showQRCodeScanner: true)
.navigationTitle("New chat")
.modifier(ThemedBackground(grouped: true))
.navigationBarTitleDisplayMode(.large)
@@ -196,7 +191,7 @@ func chatContactType(chat: Chat) -> ContactType {
case .contactRequest:
return .request
case let .direct(contact):
if contact.activeConn == nil && contact.profile.contactLink != nil {
if contact.activeConn == nil && contact.profile.contactLink != nil && contact.active {
return .card
} else if contact.chatDeleted {
return .chatDeleted
+58 -38
View File
@@ -45,32 +45,33 @@ enum NewChatOption: Identifiable {
var id: Self { self }
}
func cleanupPendingConnection(chatModel: ChatModel, contactConnection: PendingContactConnection?) -> SomeAlert? {
var alert: SomeAlert? = nil
if !(chatModel.showingInvitation?.connChatUsed ?? true),
let conn = contactConnection {
alert = SomeAlert(
alert: 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: []
))
func showKeepInvitationAlert() {
if let showingInvitation = ChatModel.shared.showingInvitation,
!showingInvitation.connChatUsed {
showAlert(
NSLocalizedString("Keep unused invitation?", comment: "alert title"),
message: NSLocalizedString("You can view invitation link again in connection details.", comment: "alert message"),
actions: {[
UIAlertAction(
title: NSLocalizedString("Keep", comment: "alert action"),
style: .default
),
UIAlertAction(
title: NSLocalizedString("Delete", comment: "alert action"),
style: .destructive,
handler: { _ in
Task {
await deleteChat(Chat(
chatInfo: .contactConnection(contactConnection: showingInvitation.pcc),
chatItems: []
))
}
}
}
),
id: "keepUnusedInvitation"
)
]}
)
}
chatModel.showingInvitation = nil
return alert
ChatModel.shared.showingInvitation = nil
}
struct NewChatView: View {
@@ -84,19 +85,23 @@ struct NewChatView: View {
@State var choosingProfile = false
@State private var pastedLink: String = ""
@State private var alert: NewChatViewAlert?
@Binding var parentAlert: SomeAlert?
@Binding var contactConnection: PendingContactConnection?
@State private var contactConnection: PendingContactConnection? = nil
var body: some View {
VStack(alignment: .leading) {
Picker("New chat", selection: $selection) {
Label("Add contact", systemImage: "link")
Label("1-time link", systemImage: "link")
.tag(NewChatOption.invite)
Label("Connect via link", systemImage: "qrcode")
.tag(NewChatOption.connect)
}
.pickerStyle(.segmented)
.padding()
.onChange(of: $selection.wrappedValue) { opt in
if opt == NewChatOption.connect {
showQRCodeScanner = true
}
}
VStack {
// it seems there's a bug in iOS 15 if several views in switch (or if-else) statement have different transitions
@@ -152,7 +157,7 @@ struct NewChatView: View {
}
.onDisappear {
if !choosingProfile {
parentAlert = cleanupPendingConnection(chatModel: m, contactConnection: contactConnection)
showKeepInvitationAlert()
contactConnection = nil
}
}
@@ -192,7 +197,7 @@ struct NewChatView: View {
if let (connReq, pcc) = r {
await MainActor.run {
m.updateContactConnection(pcc)
m.showingInvitation = ShowingInvitation(connId: pcc.id, connChatUsed: false)
m.showingInvitation = ShowingInvitation(pcc: pcc, connChatUsed: false)
connReqInvitation = connReq
contactConnection = pcc
}
@@ -241,7 +246,6 @@ private struct InviteView: View {
@Binding var choosingProfile: Bool
@AppStorage(GROUP_DEFAULT_INCOGNITO, store: groupDefaults) private var incognitoDefault = false
@State private var showSettings: Bool = false
var body: some View {
List {
@@ -585,6 +589,7 @@ private struct ConnectView: View {
@Binding var pastedLink: String
@Binding var alert: NewChatViewAlert?
@State private var sheet: PlanAndConnectActionSheet?
@State private var pasteboardHasStrings = UIPasteboard.general.hasStrings
var body: some View {
List {
@@ -621,7 +626,7 @@ private struct ConnectView: View {
} label: {
Text("Tap to paste link")
}
.disabled(!ChatModel.shared.pasteboardHasStrings)
.disabled(!pasteboardHasStrings)
.frame(maxWidth: .infinity, alignment: .center)
} else {
linkTextView(pastedLink)
@@ -692,6 +697,7 @@ struct ScannerInView: View {
.frame(maxWidth: .infinity, maxHeight: .infinity)
.foregroundColor(Color.clear)
switch cameraAuthorizationStatus {
case .authorized, nil: EmptyView()
case .restricted: Text("Camera not available")
case .denied: Label("Enable camera access", systemImage: "camera")
default: Label("Tap to scan", systemImage: "qrcode")
@@ -711,21 +717,26 @@ struct ScannerInView: View {
.disabled(cameraAuthorizationStatus == .restricted)
}
}
.onAppear {
.task {
let status = AVCaptureDevice.authorizationStatus(for: .video)
cameraAuthorizationStatus = status
if showQRCodeScanner {
switch status {
case .notDetermined: askCameraAuthorization()
case .notDetermined: await askCameraAuthorizationAsync()
case .restricted: showQRCodeScanner = false
case .denied: showQRCodeScanner = false
case .authorized: ()
@unknown default: askCameraAuthorization()
@unknown default: await askCameraAuthorizationAsync()
}
}
}
}
func askCameraAuthorizationAsync() async {
await AVCaptureDevice.requestAccess(for: .video)
cameraAuthorizationStatus = AVCaptureDevice.authorizationStatus(for: .video)
}
func askCameraAuthorization(_ cb: (() -> Void)? = nil) {
AVCaptureDevice.requestAccess(for: .video) { allowed in
cameraAuthorizationStatus = AVCaptureDevice.authorizationStatus(for: .video)
@@ -905,11 +916,17 @@ func planAndConnectAlert(_ alert: PlanAndConnectAlert, dismiss: Bool, cleanup: (
)
case let .groupLinkConnecting(_, groupInfo):
if let groupInfo = groupInfo {
return Alert(
return groupInfo.businessChat == nil
? Alert(
title: Text("Group already exists!"),
message: Text("You are already joining the group \(groupInfo.displayName)."),
dismissButton: .default(Text("OK")) { cleanup?() }
)
: Alert(
title: Text("Chat already exists!"),
message: Text("You are already connecting to \(groupInfo.displayName)."),
dismissButton: .default(Text("OK")) { cleanup?() }
)
} else {
return Alert(
title: Text("Already joining the group!"),
@@ -1226,10 +1243,15 @@ func contactAlreadyConnectingAlert(_ contact: Contact) -> Alert {
}
func groupAlreadyExistsAlert(_ groupInfo: GroupInfo) -> Alert {
mkAlert(
groupInfo.businessChat == nil
? mkAlert(
title: "Group already exists",
message: "You are already in group \(groupInfo.displayName)."
)
: mkAlert(
title: "Chat already exists",
message: "You are already connected with \(groupInfo.displayName)."
)
}
enum ConnReqType: Equatable {
@@ -1267,9 +1289,7 @@ struct NewChatView_Previews: PreviewProvider {
@State var contactConnection: PendingContactConnection? = nil
NewChatView(
selection: .invite,
parentAlert: $parentAlert,
contactConnection: $contactConnection
selection: .invite
)
}
}
+23 -23
View File
@@ -49,34 +49,34 @@ struct QRCode: View {
ZStack {
if let image = image {
qrCodeImage(image)
}
GeometryReader { geo in
ZStack {
if withLogo {
let w = geo.size.width
Image("icon-light")
.resizable()
.scaledToFit()
.frame(width: w * 0.16, height: w * 0.16)
.frame(width: w * 0.165, height: w * 0.165)
.background(.white)
.clipShape(Circle())
GeometryReader { geo in
ZStack {
if withLogo {
let w = geo.size.width
Image("icon-light")
.resizable()
.scaledToFit()
.frame(width: w * 0.16, height: w * 0.16)
.frame(width: w * 0.165, height: w * 0.165)
.background(.white)
.clipShape(Circle())
}
}
}
.onAppear {
makeScreenshotFunc = {
let size = CGSizeMake(1024 / UIScreen.main.scale, 1024 / UIScreen.main.scale)
showShareSheet(items: [makeScreenshot(geo.frame(in: .local).origin, size)])
onShare?()
.onAppear {
makeScreenshotFunc = {
let size = CGSizeMake(1024 / UIScreen.main.scale, 1024 / UIScreen.main.scale)
showShareSheet(items: [makeScreenshot(geo.frame(in: .local).origin, size)])
onShare?()
}
}
.frame(width: geo.size.width, height: geo.size.height)
}
.frame(width: geo.size.width, height: geo.size.height)
} else {
Color.clear.aspectRatio(1, contentMode: .fit)
}
}
.onTapGesture(perform: makeScreenshotFunc)
.onAppear {
image = image ?? generateImage(uri, tintColor: tintColor)
}
.task { image = await generateImage(uri, tintColor: tintColor) }
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
@@ -89,7 +89,7 @@ private func qrCodeImage(_ image: UIImage) -> some View {
.textSelection(.enabled)
}
private func generateImage(_ uri: String, tintColor: UIColor) -> UIImage? {
private func generateImage(_ uri: String, tintColor: UIColor) async -> UIImage? {
let context = CIContext()
let filter = CIFilter.qrCodeGenerator()
filter.message = Data(uri.utf8)
@@ -0,0 +1,109 @@
//
// AddressCreationCard.swift
// SimpleX (iOS)
//
// Created by Diogo Cunha on 13/11/2024.
// Copyright © 2024 SimpleX Chat. All rights reserved.
//
import SwiftUI
import SimpleXChat
struct AddressCreationCard: View {
@EnvironmentObject var theme: AppTheme
@EnvironmentObject private var chatModel: ChatModel
@Environment(\.dynamicTypeSize) private var userFont: DynamicTypeSize
@AppStorage(DEFAULT_ADDRESS_CREATION_CARD_SHOWN) private var addressCreationCardShown = false
@State private var showAddressCreationAlert = false
@State private var showAddressSheet = false
@State private var showAddressInfoSheet = false
var body: some View {
let addressExists = chatModel.userAddress != nil
let chats = chatModel.chats.filter { chat in
!chat.chatInfo.chatDeleted && chatContactType(chat: chat) != ContactType.card
}
ZStack(alignment: .topTrailing) {
HStack(alignment: .top, spacing: 16) {
let envelopeSize = dynamicSize(userFont).profileImageSize
Image(systemName: "envelope.circle.fill")
.resizable()
.frame(width: envelopeSize, height: envelopeSize)
.foregroundColor(.accentColor)
VStack(alignment: .leading) {
Text("Your SimpleX address")
.font(.title3)
Spacer()
Text("How to use it") + textSpace + Text(Image(systemName: "info.circle")).foregroundColor(theme.colors.secondary)
}
}
.frame(maxWidth: .infinity, alignment: .leading)
VStack(alignment: .trailing) {
Image(systemName: "multiply")
.foregroundColor(theme.colors.secondary)
.onTapGesture {
showAddressCreationAlert = true
}
Spacer()
Text("Create")
.foregroundColor(.accentColor)
.onTapGesture {
showAddressSheet = true
}
}
}
.onTapGesture {
showAddressInfoSheet = true
}
.padding()
.background(theme.appColors.sentMessage)
.cornerRadius(12)
.frame(height: dynamicSize(userFont).rowHeight)
.alert(isPresented: $showAddressCreationAlert) {
Alert(
title: Text("SimpleX address"),
message: Text("Tap Create SimpleX address in the menu to create it later."),
dismissButton: .default(Text("Ok")) {
withAnimation {
addressCreationCardShown = true
}
}
)
}
.sheet(isPresented: $showAddressSheet) {
NavigationView {
UserAddressView(autoCreate: true)
.navigationTitle("SimpleX address")
.navigationBarTitleDisplayMode(.large)
.modifier(ThemedBackground(grouped: true))
}
}
.sheet(isPresented: $showAddressInfoSheet) {
NavigationView {
UserAddressLearnMore(showCreateAddressButton: true)
.navigationTitle("Address or 1-time link?")
.navigationBarTitleDisplayMode(.inline)
.modifier(ThemedBackground(grouped: true))
}
}
.onChange(of: addressExists) { exists in
if exists, !addressCreationCardShown {
addressCreationCardShown = true
}
}
.onChange(of: chats.count) { size in
if size >= 3, !addressCreationCardShown {
addressCreationCardShown = true
}
}
.onAppear {
if addressExists, !addressCreationCardShown {
addressCreationCardShown = true
}
}
}
}
#Preview {
AddressCreationCard()
}
@@ -0,0 +1,451 @@
//
// ChooseServerOperators.swift
// SimpleX (iOS)
//
// Created by spaced4ndy on 31.10.2024.
// Copyright © 2024 SimpleX Chat. All rights reserved.
//
import SwiftUI
import SimpleXChat
let conditionsURL = URL(string: "https://github.com/simplex-chat/simplex-chat/blob/stable/PRIVACY.md")!
struct OnboardingButtonStyle: ButtonStyle {
@EnvironmentObject var theme: AppTheme
var isDisabled: Bool = false
func makeBody(configuration: Configuration) -> some View {
configuration.label
.font(.system(size: 17, weight: .semibold))
.padding()
.frame(maxWidth: .infinity)
.background(
isDisabled
? (
theme.colors.isLight
? .gray.opacity(0.17)
: .gray.opacity(0.27)
)
: theme.colors.primary
)
.foregroundColor(
isDisabled
? (
theme.colors.isLight
? .gray.opacity(0.4)
: .white.opacity(0.2)
)
: .white
)
.cornerRadius(16)
.scaleEffect(configuration.isPressed ? 0.95 : 1.0)
}
}
private enum ChooseServerOperatorsSheet: Identifiable {
case showInfo
case showConditions
var id: String {
switch self {
case .showInfo: return "showInfo"
case .showConditions: return "showConditions"
}
}
}
struct ChooseServerOperators: View {
@Environment(\.dismiss) var dismiss: DismissAction
@Environment(\.colorScheme) var colorScheme: ColorScheme
@EnvironmentObject var theme: AppTheme
var onboarding: Bool
@State private var serverOperators: [ServerOperator] = []
@State private var selectedOperatorIds = Set<Int64>()
@State private var sheetItem: ChooseServerOperatorsSheet? = nil
@State private var notificationsModeNavLinkActive = false
@State private var justOpened = true
var selectedOperators: [ServerOperator] { serverOperators.filter { selectedOperatorIds.contains($0.operatorId) } }
var body: some View {
GeometryReader { g in
ScrollView {
VStack(alignment: .leading, spacing: 20) {
let title = Text("Server operators")
.font(.largeTitle)
.bold()
.frame(maxWidth: .infinity, alignment: .center)
if onboarding {
title.padding(.top, 25)
} else {
title
}
infoText()
.frame(maxWidth: .infinity, alignment: .center)
Spacer()
ForEach(serverOperators) { srvOperator in
operatorCheckView(srvOperator)
}
VStack {
Text("SimpleX Chat and Flux made an agreement to include Flux-operated servers into the app.").padding(.bottom, 8)
Text("You can configure servers via settings.")
}
.font(.footnote)
.multilineTextAlignment(.center)
.frame(maxWidth: .infinity, alignment: .center)
.padding(.horizontal, 16)
Spacer()
let reviewForOperators = selectedOperators.filter { !$0.conditionsAcceptance.conditionsAccepted }
let canReviewLater = reviewForOperators.allSatisfy { $0.conditionsAcceptance.usageAllowed }
let currEnabledOperatorIds = Set(serverOperators.filter { $0.enabled }.map { $0.operatorId })
VStack(spacing: 8) {
if !reviewForOperators.isEmpty {
reviewConditionsButton()
} else if selectedOperatorIds != currEnabledOperatorIds && !selectedOperatorIds.isEmpty {
setOperatorsButton()
} else {
continueButton()
}
if onboarding {
Group {
if reviewForOperators.isEmpty {
Button("Conditions of use") {
sheetItem = .showConditions
}
} else {
Text("Conditions of use")
.foregroundColor(.clear)
}
}
.font(.system(size: 17, weight: .semibold))
.frame(minHeight: 40)
}
}
if !onboarding && !reviewForOperators.isEmpty {
VStack(spacing: 8) {
reviewLaterButton()
(
Text("Conditions will be accepted for enabled operators after 30 days.")
+ textSpace
+ Text("You can configure operators in Network & servers settings.")
)
.multilineTextAlignment(.center)
.font(.footnote)
.padding(.horizontal, 32)
}
.frame(maxWidth: .infinity)
.disabled(!canReviewLater)
.padding(.bottom)
}
}
.frame(minHeight: g.size.height)
}
.onAppear {
if justOpened {
serverOperators = ChatModel.shared.conditions.serverOperators
selectedOperatorIds = Set(serverOperators.filter { $0.enabled }.map { $0.operatorId })
justOpened = false
}
}
.sheet(item: $sheetItem) { item in
switch item {
case .showInfo:
ChooseServerOperatorsInfoView()
case .showConditions:
UsageConditionsView(
currUserServers: Binding.constant([]),
userServers: Binding.constant([])
)
.modifier(ThemedBackground(grouped: true))
}
}
.frame(maxHeight: .infinity, alignment: .top)
}
.frame(maxHeight: .infinity, alignment: .top)
.padding(onboarding ? 25 : 16)
}
private func infoText() -> some View {
Button {
sheetItem = .showInfo
} label: {
Label("How it helps privacy", systemImage: "info.circle")
.font(.headline)
}
}
@ViewBuilder private func operatorCheckView(_ serverOperator: ServerOperator) -> some View {
let checked = selectedOperatorIds.contains(serverOperator.operatorId)
let icon = checked ? "checkmark.circle.fill" : "circle"
let iconColor = checked ? theme.colors.primary : Color(uiColor: .tertiaryLabel).asAnotherColorFromSecondary(theme)
HStack(spacing: 10) {
Image(serverOperator.largeLogo(colorScheme))
.resizable()
.scaledToFit()
.frame(height: 48)
Spacer()
Image(systemName: icon)
.resizable()
.scaledToFit()
.frame(width: 26, height: 26)
.foregroundColor(iconColor)
}
.background(theme.colors.background)
.padding()
.clipShape(RoundedRectangle(cornerRadius: 18))
.overlay(
RoundedRectangle(cornerRadius: 18)
.stroke(Color(uiColor: .secondarySystemFill), lineWidth: 2)
)
.padding(.horizontal, 2)
.onTapGesture {
if checked {
selectedOperatorIds.remove(serverOperator.operatorId)
} else {
selectedOperatorIds.insert(serverOperator.operatorId)
}
}
}
private func reviewConditionsButton() -> some View {
NavigationLink("Review conditions") {
reviewConditionsView()
.navigationTitle("Conditions of use")
.navigationBarTitleDisplayMode(.large)
.toolbar { ToolbarItem(placement: .navigationBarTrailing, content: conditionsLinkButton) }
.modifier(ThemedBackground(grouped: true))
}
.buttonStyle(OnboardingButtonStyle(isDisabled: selectedOperatorIds.isEmpty))
.disabled(selectedOperatorIds.isEmpty)
}
private func setOperatorsButton() -> some View {
notificationsModeNavLinkButton {
Button {
Task {
if let enabledOperators = enabledOperators(serverOperators) {
let r = try await setServerOperators(operators: enabledOperators)
await MainActor.run {
ChatModel.shared.conditions = r
continueToNextStep()
}
} else {
await MainActor.run {
continueToNextStep()
}
}
}
} label: {
Text("Update")
}
.buttonStyle(OnboardingButtonStyle(isDisabled: selectedOperatorIds.isEmpty))
.disabled(selectedOperatorIds.isEmpty)
}
}
private func continueButton() -> some View {
notificationsModeNavLinkButton {
Button {
continueToNextStep()
} label: {
Text("Continue")
}
.buttonStyle(OnboardingButtonStyle(isDisabled: selectedOperatorIds.isEmpty))
.disabled(selectedOperatorIds.isEmpty)
}
}
private func reviewLaterButton() -> some View {
notificationsModeNavLinkButton {
Button {
continueToNextStep()
} label: {
Text("Review later")
}
.buttonStyle(.borderless)
}
}
private func continueToNextStep() {
if onboarding {
onboardingStageDefault.set(.step4_SetNotificationsMode)
notificationsModeNavLinkActive = true
} else {
dismiss()
}
}
func notificationsModeNavLinkButton(_ button: @escaping (() -> some View)) -> some View {
ZStack {
button()
NavigationLink(isActive: $notificationsModeNavLinkActive) {
notificationsModeDestinationView()
} label: {
EmptyView()
}
.frame(width: 1, height: 1)
.hidden()
}
}
private func notificationsModeDestinationView() -> some View {
SetNotificationsMode()
.navigationBarBackButtonHidden(true)
.modifier(ThemedBackground())
}
@ViewBuilder private func reviewConditionsView() -> some View {
let operatorsWithConditionsAccepted = ChatModel.shared.conditions.serverOperators.filter { $0.conditionsAcceptance.conditionsAccepted }
let acceptForOperators = selectedOperators.filter { !$0.conditionsAcceptance.conditionsAccepted }
VStack(alignment: .leading, spacing: 20) {
if !operatorsWithConditionsAccepted.isEmpty {
Text("Conditions are already accepted for these operator(s): **\(operatorsWithConditionsAccepted.map { $0.legalName_ }.joined(separator: ", "))**.")
Text("The same conditions will apply to operator(s): **\(acceptForOperators.map { $0.legalName_ }.joined(separator: ", "))**.")
} else {
Text("Conditions will be accepted for operator(s): **\(acceptForOperators.map { $0.legalName_ }.joined(separator: ", "))**.")
}
ConditionsTextView()
.frame(maxHeight: .infinity)
acceptConditionsButton()
.padding(.bottom)
.padding(.bottom)
}
.padding(.horizontal, 25)
}
private func acceptConditionsButton() -> some View {
notificationsModeNavLinkButton {
Button {
Task {
do {
let conditionsId = ChatModel.shared.conditions.currentConditions.conditionsId
let acceptForOperators = selectedOperators.filter { !$0.conditionsAcceptance.conditionsAccepted }
let operatorIds = acceptForOperators.map { $0.operatorId }
let r = try await acceptConditions(conditionsId: conditionsId, operatorIds: operatorIds)
await MainActor.run {
ChatModel.shared.conditions = r
}
if let enabledOperators = enabledOperators(r.serverOperators) {
let r2 = try await setServerOperators(operators: enabledOperators)
await MainActor.run {
ChatModel.shared.conditions = r2
continueToNextStep()
}
} else {
await MainActor.run {
continueToNextStep()
}
}
} catch let error {
await MainActor.run {
showAlert(
NSLocalizedString("Error accepting conditions", comment: "alert title"),
message: responseError(error)
)
}
}
}
} label: {
Text("Accept conditions")
}
.buttonStyle(OnboardingButtonStyle())
}
}
private func enabledOperators(_ operators: [ServerOperator]) -> [ServerOperator]? {
var ops = operators
if !ops.isEmpty {
for i in 0..<ops.count {
var op = ops[i]
op.enabled = selectedOperatorIds.contains(op.operatorId)
ops[i] = op
}
let haveSMPStorage = ops.contains(where: { $0.enabled && $0.smpRoles.storage })
let haveSMPProxy = ops.contains(where: { $0.enabled && $0.smpRoles.proxy })
let haveXFTPStorage = ops.contains(where: { $0.enabled && $0.xftpRoles.storage })
let haveXFTPProxy = ops.contains(where: { $0.enabled && $0.xftpRoles.proxy })
if haveSMPStorage && haveSMPProxy && haveXFTPStorage && haveXFTPProxy {
return ops
} else if let firstEnabledIndex = ops.firstIndex(where: { $0.enabled }) {
var op = ops[firstEnabledIndex]
if !haveSMPStorage { op.smpRoles.storage = true }
if !haveSMPProxy { op.smpRoles.proxy = true }
if !haveXFTPStorage { op.xftpRoles.storage = true }
if !haveXFTPProxy { op.xftpRoles.proxy = true }
ops[firstEnabledIndex] = op
return ops
} else { // Shouldn't happen - view doesn't let to proceed if no operators are enabled
return nil
}
} else {
return nil
}
}
}
let operatorsPostLink = URL(string: "https://simplex.chat/blog/20241125-servers-operated-by-flux-true-privacy-and-decentralization-for-all-users.html")!
struct ChooseServerOperatorsInfoView: View {
@Environment(\.colorScheme) var colorScheme: ColorScheme
@EnvironmentObject var theme: AppTheme
var body: some View {
NavigationView {
List {
VStack(alignment: .leading, spacing: 12) {
Text("The app protects your privacy by using different operators in each conversation.")
Text("When more than one operator is enabled, none of them has metadata to learn who communicates with whom.")
Text("For example, if your contact receives messages via a SimpleX Chat server, your app will deliver them via a Flux server.")
}
.fixedSize(horizontal: false, vertical: true)
.listRowBackground(Color.clear)
.listRowSeparator(.hidden)
.listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
.padding(.top)
Section {
ForEach(ChatModel.shared.conditions.serverOperators) { op in
operatorInfoNavLinkView(op)
}
} header: {
Text("About operators")
.foregroundColor(theme.colors.secondary)
}
}
.navigationTitle("Server operators")
.navigationBarTitleDisplayMode(.large)
.modifier(ThemedBackground(grouped: true))
}
}
private func operatorInfoNavLinkView(_ op: ServerOperator) -> some View {
NavigationLink() {
OperatorInfoView(serverOperator: op)
.navigationBarTitle("Network operator")
.modifier(ThemedBackground(grouped: true))
.navigationBarTitleDisplayMode(.large)
} label: {
HStack {
Image(op.logo(colorScheme))
.resizable()
.scaledToFit()
.frame(width: 24, height: 24)
Text(op.tradeName)
}
}
}
}
#Preview {
ChooseServerOperators(onboarding: true)
}
@@ -38,7 +38,7 @@ struct CreateProfile: View {
TextField("Enter your name…", text: $displayName)
.focused($focusDisplayName)
Button {
createProfile(displayName, showAlert: { alert = $0 }, dismiss: dismiss)
createProfile()
} label: {
Label("Create profile", systemImage: "checkmark")
}
@@ -78,6 +78,35 @@ struct CreateProfile: View {
}
}
}
private func createProfile() {
hideKeyboard()
let profile = Profile(
displayName: displayName.trimmingCharacters(in: .whitespaces),
fullName: ""
)
let m = ChatModel.shared
do {
AppChatState.shared.set(.active)
m.currentUser = try apiCreateActiveUser(profile)
// .isEmpty check is redundant here, but it makes it clearer what is going on
if m.users.isEmpty || m.users.allSatisfy({ $0.user.hidden }) {
try startChat()
withAnimation {
onboardingStageDefault.set(.step3_ChooseServerOperators)
m.onboardingStage = .step3_ChooseServerOperators
}
} else {
onboardingStageDefault.set(.onboardingComplete)
m.onboardingStage = .onboardingComplete
dismiss()
m.users = try listUsers()
try getUserChatData()
}
} catch let error {
showCreateProfileAlert(showAlert: { alert = $0 }, error)
}
}
}
struct CreateFirstProfile: View {
@@ -86,131 +115,150 @@ struct CreateFirstProfile: View {
@Environment(\.dismiss) var dismiss
@State private var displayName: String = ""
@FocusState private var focusDisplayName
@State private var nextStepNavLinkActive = false
var body: some View {
VStack(alignment: .leading) {
Group {
VStack(alignment: .leading, spacing: 20) {
VStack(alignment: .center, spacing: 20) {
Text("Create your profile")
.font(.largeTitle)
.bold()
.multilineTextAlignment(.center)
Text("Your profile, contacts and delivered messages are stored on your device.")
.font(.callout)
.foregroundColor(theme.colors.secondary)
.multilineTextAlignment(.center)
Text("The profile is only shared with your contacts.")
.font(.callout)
.foregroundColor(theme.colors.secondary)
.padding(.bottom)
.multilineTextAlignment(.center)
}
.padding(.bottom)
.frame(maxWidth: .infinity) // Ensures it takes up the full width
.padding(.horizontal, 10)
ZStack(alignment: .topLeading) {
HStack {
let name = displayName.trimmingCharacters(in: .whitespaces)
let validName = mkValidName(name)
if name != validName {
Button {
showAlert(.invalidNameError(validName: validName))
} label: {
Image(systemName: "exclamationmark.circle").foregroundColor(.red)
ZStack(alignment: .trailing) {
TextField("Enter your name…", text: $displayName)
.focused($focusDisplayName)
.padding(.horizontal)
.padding(.vertical, 10)
.background(
RoundedRectangle(cornerRadius: 10, style: .continuous)
.fill(Color(uiColor: .tertiarySystemFill))
)
if name != validName {
Button {
showAlert(.invalidNameError(validName: validName))
} label: {
Image(systemName: "exclamationmark.circle")
.foregroundColor(.red)
.padding(.horizontal, 10)
}
}
} else {
Image(systemName: "exclamationmark.circle").foregroundColor(.clear)
}
TextField("Enter your name…", text: $displayName)
.focused($focusDisplayName)
.padding(.leading, 32)
}
.padding(.bottom)
.padding(.top)
Spacer()
onboardingButtons()
VStack(spacing: 10) {
createProfileButton()
if !focusDisplayName {
onboardingButtonPlaceholder()
}
}
}
.onAppear() {
focusDisplayName = true
setLastVersionDefault()
}
.padding()
.padding(.horizontal, 25)
.padding(.top, 10)
.padding(.bottom, 25)
.frame(maxWidth: .infinity, alignment: .leading)
}
func onboardingButtons() -> some View {
HStack {
func createProfileButton() -> some View {
ZStack {
Button {
hideKeyboard()
withAnimation {
m.onboardingStage = .step1_SimpleXInfo
}
createProfile()
} label: {
HStack {
Image(systemName: "lessthan")
Text("About SimpleX")
}
}
Spacer()
Button {
createProfile(displayName, showAlert: showAlert, dismiss: dismiss)
} label: {
HStack {
Text("Create")
Image(systemName: "greaterthan")
}
Text("Create profile")
}
.buttonStyle(OnboardingButtonStyle(isDisabled: !canCreateProfile(displayName)))
.disabled(!canCreateProfile(displayName))
NavigationLink(isActive: $nextStepNavLinkActive) {
nextStepDestinationView()
} label: {
EmptyView()
}
.frame(width: 1, height: 1)
.hidden()
}
}
private func showAlert(_ alert: UserProfileAlert) {
AlertManager.shared.showAlert(userProfileAlert(alert, $displayName))
}
private func nextStepDestinationView() -> some View {
ChooseServerOperators(onboarding: true)
.navigationBarBackButtonHidden(true)
.modifier(ThemedBackground())
}
private func createProfile() {
hideKeyboard()
let profile = Profile(
displayName: displayName.trimmingCharacters(in: .whitespaces),
fullName: ""
)
let m = ChatModel.shared
do {
AppChatState.shared.set(.active)
m.currentUser = try apiCreateActiveUser(profile)
try startChat(onboarding: true)
onboardingStageDefault.set(.step3_ChooseServerOperators)
nextStepNavLinkActive = true
} catch let error {
showCreateProfileAlert(showAlert: showAlert, error)
}
}
}
private func createProfile(_ displayName: String, showAlert: (UserProfileAlert) -> Void, dismiss: DismissAction) {
hideKeyboard()
let profile = Profile(
displayName: displayName.trimmingCharacters(in: .whitespaces),
fullName: ""
)
private func showCreateProfileAlert(
showAlert: (UserProfileAlert) -> Void,
_ error: Error
) {
let m = ChatModel.shared
do {
AppChatState.shared.set(.active)
m.currentUser = try apiCreateActiveUser(profile)
// .isEmpty check is redundant here, but it makes it clearer what is going on
if m.users.isEmpty || m.users.allSatisfy({ $0.user.hidden }) {
try startChat()
withAnimation {
onboardingStageDefault.set(.step3_CreateSimpleXAddress)
m.onboardingStage = .step3_CreateSimpleXAddress
}
switch error as? ChatResponse {
case .chatCmdError(_, .errorStore(.duplicateName)),
.chatCmdError(_, .error(.userExists)):
if m.currentUser == nil {
AlertManager.shared.showAlert(duplicateUserAlert)
} else {
onboardingStageDefault.set(.onboardingComplete)
m.onboardingStage = .onboardingComplete
dismiss()
m.users = try listUsers()
try getUserChatData()
showAlert(.duplicateUserError)
}
} catch let error {
switch error as? ChatResponse {
case .chatCmdError(_, .errorStore(.duplicateName)),
.chatCmdError(_, .error(.userExists)):
if m.currentUser == nil {
AlertManager.shared.showAlert(duplicateUserAlert)
} 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 {
AlertManager.shared.showAlert(creatUserErrorAlert(err))
} else {
showAlert(.createUserError(error: err))
}
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 {
AlertManager.shared.showAlert(creatUserErrorAlert(err))
} else {
showAlert(.createUserError(error: err))
}
logger.error("Failed to create user or start chat: \(responseError(error))")
}
logger.error("Failed to create user or start chat: \(responseError(error))")
}
private func canCreateProfile(_ displayName: String) -> Bool {
@@ -9,24 +9,24 @@
import SwiftUI
struct HowItWorks: View {
@Environment(\.dismiss) var dismiss: DismissAction
@EnvironmentObject var m: ChatModel
var onboarding: Bool
@Binding var createProfileNavLinkActive: Bool
var body: some View {
VStack(alignment: .leading) {
Text("How SimpleX works")
.font(.largeTitle)
.bold()
.padding(.vertical)
ScrollView {
VStack(alignment: .leading) {
Group {
Text("Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?*")
Text("To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts.")
Text("You control through which server(s) **to receive** the messages, your contacts the servers you use to message them.")
Text("Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**.")
if onboarding {
Text("Read more in our GitHub repository.")
} else {
Text("To protect your privacy, SimpleX uses separate IDs for each of your contacts.")
Text("Only client devices store user profiles, contacts, groups, and messages.")
Text("All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages.")
if !onboarding {
Text("Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme).")
}
}
@@ -37,19 +37,34 @@ struct HowItWorks: View {
Spacer()
if onboarding {
OnboardingActionButton()
.padding(.bottom, 8)
VStack(spacing: 10) {
createFirstProfileButton()
onboardingButtonPlaceholder()
}
}
}
.lineLimit(10)
.padding()
.padding(onboarding ? 25 : 16)
.frame(maxHeight: .infinity, alignment: .top)
.modifier(ThemedBackground())
}
private func createFirstProfileButton() -> some View {
Button {
dismiss()
createProfileNavLinkActive = true
} label: {
Text("Create your profile")
}
.buttonStyle(OnboardingButtonStyle(isDisabled: false))
}
}
struct HowItWorks_Previews: PreviewProvider {
static var previews: some View {
HowItWorks(onboarding: true)
HowItWorks(
onboarding: true,
createProfileNavLinkActive: Binding.constant(false)
)
}
}
@@ -12,20 +12,39 @@ struct OnboardingView: View {
var onboarding: OnboardingStage
var body: some View {
switch onboarding {
case .step1_SimpleXInfo: SimpleXInfo(onboarding: true)
case .step2_CreateProfile: CreateFirstProfile()
case .step3_CreateSimpleXAddress: CreateSimpleXAddress()
case .step4_SetNotificationsMode: SetNotificationsMode()
case .onboardingComplete: EmptyView()
NavigationView {
switch onboarding {
case .step1_SimpleXInfo:
SimpleXInfo(onboarding: true)
.modifier(ThemedBackground())
case .step2_CreateProfile: // deprecated
CreateFirstProfile()
.modifier(ThemedBackground())
case .step3_CreateSimpleXAddress: // deprecated
CreateSimpleXAddress()
case .step3_ChooseServerOperators:
ChooseServerOperators(onboarding: true)
.navigationBarBackButtonHidden(true)
.modifier(ThemedBackground())
case .step4_SetNotificationsMode:
SetNotificationsMode()
.navigationBarBackButtonHidden(true)
.modifier(ThemedBackground())
case .onboardingComplete: EmptyView()
}
}
}
}
func onboardingButtonPlaceholder() -> some View {
Spacer().frame(height: 40)
}
enum OnboardingStage: String, Identifiable {
case step1_SimpleXInfo
case step2_CreateProfile
case step3_CreateSimpleXAddress
case step2_CreateProfile // deprecated
case step3_CreateSimpleXAddress // deprecated
case step3_ChooseServerOperators
case step4_SetNotificationsMode
case onboardingComplete
@@ -13,42 +13,54 @@ struct SetNotificationsMode: View {
@EnvironmentObject var m: ChatModel
@State private var notificationMode = NotificationsMode.instant
@State private var showAlert: NotificationAlert?
@State private var showInfo: Bool = false
var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: 16) {
Text("Push notifications")
.font(.largeTitle)
.bold()
.frame(maxWidth: .infinity)
GeometryReader { g in
ScrollView {
VStack(alignment: .center, spacing: 20) {
Text("Push notifications")
.font(.largeTitle)
.bold()
.padding(.top, 25)
infoText()
Spacer()
Text("Send notifications:")
ForEach(NotificationsMode.values) { mode in
NtfModeSelector(mode: mode, selection: $notificationMode)
}
Spacer()
Button {
if let token = m.deviceToken {
setNotificationsMode(token, notificationMode)
} else {
AlertManager.shared.showAlertMsg(title: "No device token!")
ForEach(NotificationsMode.values) { mode in
NtfModeSelector(mode: mode, selection: $notificationMode)
}
onboardingStageDefault.set(.onboardingComplete)
m.onboardingStage = .onboardingComplete
} label: {
if case .off = notificationMode {
Text("Use chat")
} else {
Text("Enable notifications")
Spacer()
VStack(spacing: 10) {
Button {
if let token = m.deviceToken {
setNotificationsMode(token, notificationMode)
} else {
AlertManager.shared.showAlertMsg(title: "No device token!")
}
onboardingStageDefault.set(.onboardingComplete)
m.onboardingStage = .onboardingComplete
} label: {
if case .off = notificationMode {
Text("Use chat")
} else {
Text("Enable notifications")
}
}
.buttonStyle(OnboardingButtonStyle())
onboardingButtonPlaceholder()
}
}
.font(.title)
.frame(maxWidth: .infinity)
.padding(25)
.frame(minHeight: g.size.height)
}
.padding()
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottom)
}
.frame(maxHeight: .infinity)
.sheet(isPresented: $showInfo) {
NotificationsInfoView()
}
}
@@ -75,6 +87,15 @@ struct SetNotificationsMode: View {
}
}
}
private func infoText() -> some View {
Button {
showInfo = true
} label: {
Label("How it affects privacy", systemImage: "info.circle")
.font(.headline)
}
}
}
struct NtfModeSelector: View {
@@ -85,15 +106,25 @@ struct NtfModeSelector: View {
var body: some View {
ZStack {
VStack(alignment: .leading, spacing: 4) {
Text(mode.label)
.font(.headline)
HStack(spacing: 16) {
Image(systemName: mode.icon)
.resizable()
.scaledToFill()
.frame(width: mode.icon == "bolt" ? 14 : 18, height: 18)
.foregroundColor(selection == mode ? theme.colors.primary : theme.colors.secondary)
Text(ntfModeDescription(mode))
.lineLimit(10)
.font(.subheadline)
VStack(alignment: .leading, spacing: 4) {
Text(mode.label)
.font(.headline)
.foregroundColor(selection == mode ? theme.colors.primary : theme.colors.secondary)
Text(ntfModeShortDescription(mode))
.lineLimit(2)
.font(.callout)
.fixedSize(horizontal: false, vertical: true)
}
}
.padding(12)
.padding(.vertical, 12)
.padding(.trailing, 12)
.padding(.leading, 16)
}
.frame(maxWidth: .infinity, alignment: .leading)
.background(tapped ? Color(uiColor: .secondarySystemFill) : theme.colors.background)
@@ -109,6 +140,37 @@ struct NtfModeSelector: View {
}
}
struct NotificationsInfoView: View {
var body: some View {
VStack(alignment: .leading) {
Text("Notifications privacy")
.font(.largeTitle)
.bold()
.padding(.vertical)
ScrollView {
VStack(alignment: .leading) {
Group {
ForEach(NotificationsMode.values) { mode in
VStack(alignment: .leading, spacing: 4) {
(Text(Image(systemName: mode.icon)) + textSpace + Text(mode.label))
.font(.headline)
.foregroundColor(.secondary)
Text(ntfModeDescription(mode))
.lineLimit(10)
.font(.callout)
}
}
}
.padding(.bottom)
}
}
}
.padding()
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
.modifier(ThemedBackground())
}
}
struct NotificationsModeView_Previews: PreviewProvider {
static var previews: some View {
SetNotificationsMode()
@@ -13,58 +13,58 @@ struct SimpleXInfo: View {
@EnvironmentObject var m: ChatModel
@Environment(\.colorScheme) var colorScheme: ColorScheme
@State private var showHowItWorks = false
@State private var createProfileNavLinkActive = false
var onboarding: Bool
var body: some View {
GeometryReader { g in
ScrollView {
VStack(alignment: .leading) {
Image(colorScheme == .light ? "logo" : "logo-light")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: g.size.width * 0.67)
.padding(.bottom, 8)
.frame(maxWidth: .infinity, minHeight: 48, alignment: .top)
VStack(alignment: .center, spacing: 10) {
Image(colorScheme == .light ? "logo" : "logo-light")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: g.size.width * 0.67)
.padding(.bottom, 8)
.padding(.leading, 4)
.frame(maxWidth: .infinity, minHeight: 48, alignment: .top)
Button {
showHowItWorks = true
} label: {
Label("The future of messaging", systemImage: "info.circle")
.font(.headline)
}
}
Spacer()
VStack(alignment: .leading) {
Text("The next generation of private messaging")
.font(.title2)
.padding(.bottom, 30)
.padding(.horizontal, 40)
.frame(maxWidth: .infinity)
.multilineTextAlignment(.center)
infoRow("privacy", "Privacy redefined",
"The 1st platform without any user identifiers private by design.", width: 48)
infoRow("shield", "Immune to spam and abuse",
"People can connect to you only via the links you share.", width: 46)
infoRow(colorScheme == .light ? "decentralized" : "decentralized-light", "Decentralized",
"Open-source protocol and code anybody can run the servers.", width: 44)
onboardingInfoRow("privacy", "Privacy redefined",
"No user identifiers.", width: 48)
onboardingInfoRow("shield", "Immune to spam",
"You decide who can connect.", width: 46)
onboardingInfoRow(colorScheme == .light ? "decentralized" : "decentralized-light", "Decentralized",
"Anybody can host servers.", width: 46)
}
.padding(.leading, 16)
Spacer()
if onboarding {
OnboardingActionButton()
Spacer()
VStack(spacing: 10) {
createFirstProfileButton()
Button {
m.migrationState = .pasteOrScanLink
} label: {
Label("Migrate from another device", systemImage: "tray.and.arrow.down")
.font(.subheadline)
Button {
m.migrationState = .pasteOrScanLink
} label: {
Label("Migrate from another device", systemImage: "tray.and.arrow.down")
.font(.system(size: 17, weight: .semibold))
.frame(minHeight: 40)
}
.frame(maxWidth: .infinity)
}
.padding(.bottom, 8)
.frame(maxWidth: .infinity)
}
Button {
showHowItWorks = true
} label: {
Label("How it works", systemImage: "info.circle")
.font(.subheadline)
}
.padding(.bottom, 8)
.frame(maxWidth: .infinity)
}
.frame(minHeight: g.size.height)
}
@@ -83,76 +83,59 @@ struct SimpleXInfo: View {
}
}
.sheet(isPresented: $showHowItWorks) {
HowItWorks(onboarding: onboarding)
HowItWorks(
onboarding: onboarding,
createProfileNavLinkActive: $createProfileNavLinkActive
)
}
}
.frame(maxHeight: .infinity)
.padding()
.padding(.horizontal, 25)
.padding(.top, 75)
.padding(.bottom, 25)
}
private func infoRow(_ image: String, _ title: LocalizedStringKey, _ text: LocalizedStringKey, width: CGFloat) -> some View {
private func onboardingInfoRow(_ image: String, _ title: LocalizedStringKey, _ text: LocalizedStringKey, width: CGFloat) -> some View {
HStack(alignment: .top) {
Image(image)
.resizable()
.scaledToFit()
.frame(width: width, height: 54)
.frame(width: 54)
.padding(.top, 4)
.padding(.leading, 4)
.padding(.trailing, 10)
VStack(alignment: .leading, spacing: 4) {
Text(title).font(.headline)
Text(text).frame(minHeight: 40, alignment: .top)
.font(.callout)
.lineLimit(3)
.fixedSize(horizontal: false, vertical: true)
}
.padding(.top, 4)
}
.padding(.bottom, 12)
}
private func createFirstProfileButton() -> some View {
ZStack {
Button {
createProfileNavLinkActive = true
} label: {
Text("Create your profile")
}
.buttonStyle(OnboardingButtonStyle(isDisabled: false))
NavigationLink(isActive: $createProfileNavLinkActive) {
CreateFirstProfile()
} label: {
EmptyView()
}
.frame(width: 1, height: 1)
.hidden()
}
.padding(.bottom, 20)
.padding(.trailing, 6)
}
}
struct OnboardingActionButton: View {
@EnvironmentObject var m: ChatModel
@Environment(\.colorScheme) var colorScheme
var body: some View {
if m.currentUser == nil {
actionButton("Create your profile", onboarding: .step2_CreateProfile)
} else {
actionButton("Make a private connection", onboarding: .onboardingComplete)
}
}
private func actionButton(_ label: LocalizedStringKey, onboarding: OnboardingStage) -> some View {
Button {
withAnimation {
onboardingStageDefault.set(onboarding)
m.onboardingStage = onboarding
}
} label: {
HStack {
Text(label).font(.title2)
Image(systemName: "greaterthan")
}
}
.frame(maxWidth: .infinity)
.padding(.bottom)
}
private func actionButton(_ label: LocalizedStringKey, action: @escaping () -> Void) -> some View {
Button {
withAnimation {
action()
}
} label: {
HStack {
Text(label).font(.title2)
Image(systemName: "greaterthan")
}
}
.frame(maxWidth: .infinity)
.padding(.bottom)
}
}
let textSpace = Text(verbatim: " ")
struct SimpleXInfo_Previews: PreviewProvider {
static var previews: some View {
@@ -7,190 +7,209 @@
//
import SwiftUI
import SimpleXChat
private struct VersionDescription {
var version: String
var post: URL?
var features: [FeatureDescription]
var features: [Feature]
}
private struct FeatureDescription {
var icon: String?
var title: LocalizedStringKey
var description: LocalizedStringKey?
private enum Feature: Identifiable {
case feature(Description)
case view(FeatureView)
var id: LocalizedStringKey {
switch self {
case let .feature(d): d.title
case let .view(v): v.title
}
}
}
private struct Description {
let icon: String?
let title: LocalizedStringKey
let description: LocalizedStringKey?
var subfeatures: [(icon: String, description: LocalizedStringKey)] = []
}
private struct FeatureView {
let icon: String?
let title: LocalizedStringKey
let view: () -> any View
}
private let versionDescriptions: [VersionDescription] = [
VersionDescription(
version: "v4.2",
post: URL(string: "https://simplex.chat/blog/20221108-simplex-chat-v4.2-security-audit-new-website.html"),
features: [
FeatureDescription(
.feature(Description(
icon: "checkmark.shield",
title: "Security assessment",
description: "SimpleX Chat security was audited by Trail of Bits."
),
FeatureDescription(
)),
.feature(Description(
icon: "person.2",
title: "Group links",
description: "Admins can create the links to join groups."
),
FeatureDescription(
)),
.feature(Description(
icon: "checkmark",
title: "Auto-accept contact requests",
description: "With optional welcome message."
),
)),
]
),
VersionDescription(
version: "v4.3",
post: URL(string: "https://simplex.chat/blog/20221206-simplex-chat-v4.3-voice-messages.html"),
features: [
FeatureDescription(
.feature(Description(
icon: "mic",
title: "Voice messages",
description: "Max 30 seconds, received instantly."
),
FeatureDescription(
)),
.feature(Description(
icon: "trash.slash",
title: "Irreversible message deletion",
description: "Your contacts can allow full message deletion."
),
FeatureDescription(
)),
.feature(Description(
icon: "externaldrive.connected.to.line.below",
title: "Improved server configuration",
description: "Add servers by scanning QR codes."
),
FeatureDescription(
)),
.feature(Description(
icon: "eye.slash",
title: "Improved privacy and security",
description: "Hide app screen in the recent apps."
),
)),
]
),
VersionDescription(
version: "v4.4",
post: URL(string: "https://simplex.chat/blog/20230103-simplex-chat-v4.4-disappearing-messages.html"),
features: [
FeatureDescription(
.feature(Description(
icon: "stopwatch",
title: "Disappearing messages",
description: "Sent messages will be deleted after set time."
),
FeatureDescription(
)),
.feature(Description(
icon: "ellipsis.circle",
title: "Live messages",
description: "Recipients see updates as you type them."
),
FeatureDescription(
)),
.feature(Description(
icon: "checkmark.shield",
title: "Verify connection security",
description: "Compare security codes with your contacts."
),
FeatureDescription(
)),
.feature(Description(
icon: "camera",
title: "GIFs and stickers",
description: "Send them from gallery or custom keyboards."
),
FeatureDescription(
)),
.feature(Description(
icon: "character",
title: "French interface",
description: "Thanks to the users contribute via Weblate!"
)
)),
]
),
VersionDescription(
version: "v4.5",
post: URL(string: "https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html"),
features: [
FeatureDescription(
.feature(Description(
icon: "person.crop.rectangle.stack",
title: "Multiple chat profiles",
description: "Different names, avatars and transport isolation."
),
FeatureDescription(
)),
.feature(Description(
icon: "rectangle.and.pencil.and.ellipsis",
title: "Message draft",
description: "Preserve the last message draft, with attachments."
),
FeatureDescription(
)),
.feature(Description(
icon: "network.badge.shield.half.filled",
title: "Transport isolation",
description: "By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)."
),
FeatureDescription(
)),
.feature(Description(
icon: "lock.doc",
title: "Private filenames",
description: "To protect timezone, image/voice files use UTC."
),
FeatureDescription(
)),
.feature(Description(
icon: "battery.25",
title: "Reduced battery usage",
description: "More improvements are coming soon!"
),
FeatureDescription(
)),
.feature(Description(
icon: "character",
title: "Italian interface",
description: "Thanks to the users [contribute via Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!"
)
)),
]
),
VersionDescription(
version: "v4.6",
post: URL(string: "https://simplex.chat/blog/20230328-simplex-chat-v4-6-hidden-profiles.html"),
features: [
FeatureDescription(
.feature(Description(
icon: "lock",
title: "Hidden chat profiles",
description: "Protect your chat profiles with a password!"
),
FeatureDescription(
)),
.feature(Description(
icon: "phone.arrow.up.right",
title: "Audio and video calls",
description: "Fully re-implemented - work in background!"
),
FeatureDescription(
)),
.feature(Description(
icon: "flag",
title: "Group moderation",
description: "Now admins can:\n- delete members' messages.\n- disable members (\"observer\" role)"
),
FeatureDescription(
)),
.feature(Description(
icon: "plus.message",
title: "Group welcome message",
description: "Set the message shown to new members!"
),
FeatureDescription(
)),
.feature(Description(
icon: "battery.50",
title: "Further reduced battery usage",
description: "More improvements are coming soon!"
),
FeatureDescription(
)),
.feature(Description(
icon: "character",
title: "Chinese and Spanish interface",
description: "Thanks to the users [contribute via Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!"
),
)),
]
),
VersionDescription(
version: "v5.0",
post: URL(string: "https://simplex.chat/blog/20230422-simplex-chat-vision-funding-v5-videos-files-passcode.html"),
features: [
FeatureDescription(
.feature(Description(
icon: "arrow.up.doc",
title: "Videos and files up to 1gb",
description: "Fast and no wait until the sender is online!"
),
FeatureDescription(
)),
.feature(Description(
icon: "lock",
title: "App passcode",
description: "Set it instead of system authentication."
),
FeatureDescription(
)),
.feature(Description(
icon: "character",
title: "Polish interface",
description: "Thanks to the users [contribute via Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!"
),
)),
]
),
// Also
@@ -200,240 +219,240 @@ private let versionDescriptions: [VersionDescription] = [
version: "v5.1",
post: URL(string: "https://simplex.chat/blog/20230523-simplex-chat-v5-1-message-reactions-self-destruct-passcode.html"),
features: [
FeatureDescription(
.feature(Description(
icon: "face.smiling",
title: "Message reactions",
description: "Finally, we have them! 🚀"
),
FeatureDescription(
)),
.feature(Description(
icon: "arrow.up.message",
title: "Better messages",
description: "- voice messages up to 5 minutes.\n- custom time to disappear.\n- editing history."
),
FeatureDescription(
)),
.feature(Description(
icon: "lock",
title: "Self-destruct passcode",
description: "All data is erased when it is entered."
),
FeatureDescription(
)),
.feature(Description(
icon: "character",
title: "Japanese interface",
description: "Thanks to the users [contribute via Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!"
),
)),
]
),
VersionDescription(
version: "v5.2",
post: URL(string: "https://simplex.chat/blog/20230722-simplex-chat-v5-2-message-delivery-receipts.html"),
features: [
FeatureDescription(
.feature(Description(
icon: "checkmark",
title: "Message delivery receipts!",
description: "The second tick we missed! ✅"
),
FeatureDescription(
)),
.feature(Description(
icon: "star",
title: "Find chats faster",
description: "Filter unread and favorite chats."
),
FeatureDescription(
)),
.feature(Description(
icon: "exclamationmark.arrow.triangle.2.circlepath",
title: "Keep your connections",
description: "Fix encryption after restoring backups."
),
FeatureDescription(
)),
.feature(Description(
icon: "stopwatch",
title: "Make one message disappear",
description: "Even when disabled in the conversation."
),
FeatureDescription(
)),
.feature(Description(
icon: "gift",
title: "A few more things",
description: "- more stable message delivery.\n- a bit better groups.\n- and more!"
),
)),
]
),
VersionDescription(
version: "v5.3",
post: URL(string: "https://simplex.chat/blog/20230925-simplex-chat-v5-3-desktop-app-local-file-encryption-directory-service.html"),
features: [
FeatureDescription(
.feature(Description(
icon: "desktopcomputer",
title: "New desktop app!",
description: "Create new profile in [desktop app](https://simplex.chat/downloads/). 💻"
),
FeatureDescription(
)),
.feature(Description(
icon: "lock",
title: "Encrypt stored files & media",
description: "App encrypts new local files (except videos)."
),
FeatureDescription(
)),
.feature(Description(
icon: "magnifyingglass",
title: "Discover and join groups",
description: "- connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)!\n- delivery receipts (up to 20 members).\n- faster and more stable."
),
FeatureDescription(
)),
.feature(Description(
icon: "theatermasks",
title: "Simplified incognito mode",
description: "Toggle incognito when connecting."
),
FeatureDescription(
)),
.feature(Description(
icon: "character",
title: "\(4) new interface languages",
description: "Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!"
),
)),
]
),
VersionDescription(
version: "v5.4",
post: URL(string: "https://simplex.chat/blog/20231125-simplex-chat-v5-4-link-mobile-desktop-quantum-resistant-better-groups.html"),
features: [
FeatureDescription(
.feature(Description(
icon: "desktopcomputer",
title: "Link mobile and desktop apps! 🔗",
description: "Via secure quantum resistant protocol."
),
FeatureDescription(
)),
.feature(Description(
icon: "person.2",
title: "Better groups",
description: "Faster joining and more reliable messages."
),
FeatureDescription(
)),
.feature(Description(
icon: "theatermasks",
title: "Incognito groups",
description: "Create a group using a random profile."
),
FeatureDescription(
)),
.feature(Description(
icon: "hand.raised",
title: "Block group members",
description: "To hide unwanted messages."
),
FeatureDescription(
)),
.feature(Description(
icon: "gift",
title: "A few more things",
description: "- optionally notify deleted contacts.\n- profile names with spaces.\n- and more!"
),
)),
]
),
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(
.feature(Description(
icon: "folder",
title: "Private notes",
description: "With encrypted files and media."
),
FeatureDescription(
)),
.feature(Description(
icon: "link",
title: "Paste link to connect!",
description: "Search bar accepts invitation links."
),
FeatureDescription(
)),
.feature(Description(
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(
)),
.feature(Description(
icon: "battery.50",
title: "Improved message delivery",
description: "With reduced battery usage."
),
FeatureDescription(
)),
.feature(Description(
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)!"
),
)),
]
),
VersionDescription(
version: "v5.6",
post: URL(string: "https://simplex.chat/blog/20240323-simplex-network-privacy-non-profit-v5-6-quantum-resistant-e2e-encryption-simple-migration.html"),
features: [
FeatureDescription(
.feature(Description(
icon: "key",
title: "Quantum resistant encryption",
description: "Enable in direct chats (BETA)!"
),
FeatureDescription(
)),
.feature(Description(
icon: "tray.and.arrow.up",
title: "App data migration",
description: "Migrate to another device via QR code."
),
FeatureDescription(
)),
.feature(Description(
icon: "phone",
title: "Picture-in-picture calls",
description: "Use the app while in the call."
),
FeatureDescription(
)),
.feature(Description(
icon: "hand.raised",
title: "Safer groups",
description: "Admins can block a member for all."
),
FeatureDescription(
)),
.feature(Description(
icon: "character",
title: "Hungarian interface",
description: "Thanks to the users [contribute via Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!"
),
)),
]
),
VersionDescription(
version: "v5.7",
post: URL(string: "https://simplex.chat/blog/20240426-simplex-legally-binding-transparency-v5-7-better-user-experience.html"),
features: [
FeatureDescription(
.feature(Description(
icon: "key",
title: "Quantum resistant encryption",
description: "Will be enabled in direct chats!"
),
FeatureDescription(
)),
.feature(Description(
icon: "arrowshape.turn.up.forward",
title: "Forward and save messages",
description: "Message source remains private."
),
FeatureDescription(
)),
.feature(Description(
icon: "music.note",
title: "In-call sounds",
description: "When connecting audio and video calls."
),
FeatureDescription(
)),
.feature(Description(
icon: "person.crop.square",
title: "Shape profile images",
description: "Square, circle, or anything in between."
),
FeatureDescription(
)),
.feature(Description(
icon: "antenna.radiowaves.left.and.right",
title: "Network management",
description: "More reliable network connection."
)
)),
]
),
VersionDescription(
version: "v5.8",
post: URL(string: "https://simplex.chat/blog/20240604-simplex-chat-v5.8-private-message-routing-chat-themes.html"),
features: [
FeatureDescription(
.feature(Description(
icon: "arrow.forward",
title: "Private message routing 🚀",
description: "Protect your IP address from the messaging relays chosen by your contacts.\nEnable in *Network & servers* settings."
),
FeatureDescription(
)),
.feature(Description(
icon: "network.badge.shield.half.filled",
title: "Safely receive files",
description: "Confirm files from unknown servers."
),
FeatureDescription(
)),
.feature(Description(
icon: "battery.50",
title: "Improved message delivery",
description: "With reduced battery usage."
)
)),
]
),
VersionDescription(
version: "v6.0",
post: URL(string: "https://simplex.chat/blog/20240814-simplex-chat-vision-funding-v6-private-routing-new-user-experience.html"),
features: [
FeatureDescription(
.feature(Description(
icon: nil,
title: "New chat experience 🎉",
description: nil,
@@ -444,8 +463,8 @@ private let versionDescriptions: [VersionDescription] = [
("platter.filled.bottom.and.arrow.down.iphone", "Use the app with one hand."),
("paintpalette", "Color chats with the new themes."),
]
),
FeatureDescription(
)),
.feature(Description(
icon: nil,
title: "New media options",
description: nil,
@@ -454,19 +473,73 @@ private let versionDescriptions: [VersionDescription] = [
("play.circle", "Play from the chat list."),
("circle.filled.pattern.diagonalline.rectangle", "Blur for better privacy.")
]
),
FeatureDescription(
)),
.feature(Description(
icon: "arrow.forward",
title: "Private message routing 🚀",
description: "It protects your IP address and connections."
),
FeatureDescription(
)),
.feature(Description(
icon: "network",
title: "Better networking",
description: "Connection and servers status."
)
)),
]
),
VersionDescription(
version: "v6.1",
post: URL(string: "https://simplex.chat/blog/20241014-simplex-network-v6-1-security-review-better-calls-user-experience.html"),
features: [
.feature(Description(
icon: "checkmark.shield",
title: "Better security ✅",
description: "SimpleX protocols reviewed by Trail of Bits."
)),
.feature(Description(
icon: "video",
title: "Better calls",
description: "Switch audio and video during the call."
)),
.feature(Description(
icon: "bolt",
title: "Better notifications",
description: "Improved delivery, reduced traffic usage.\nMore improvements are coming soon!"
)),
.feature(Description(
icon: nil,
title: "Better user experience",
description: nil,
subfeatures: [
("link", "Switch chat profile for 1-time invitations."),
("message", "Customizable message shape."),
("calendar", "Better message dates."),
("arrowshape.turn.up.right", "Forward up to 20 messages at once."),
("flag", "Delete or moderate up to 200 messages.")
]
)),
]
),
VersionDescription(
version: "v6.2",
post: URL(string: "https://simplex.chat/blog/20241210-simplex-network-v6-2-servers-by-flux-business-chats.html"),
features: [
.view(FeatureView(
icon: nil,
title: "Network decentralization",
view: { NewOperatorsView() }
)),
.feature(Description(
icon: "briefcase",
title: "Business chats",
description: "Privacy for your customers."
)),
.feature(Description(
icon: "bolt",
title: "More reliable notifications",
description: "Delivered even when Apple drops them."
)),
]
)
]
private let lastVersion = versionDescriptions.last!.version
@@ -481,14 +554,68 @@ func shouldShowWhatsNew() -> Bool {
return v != lastVersion
}
fileprivate struct NewOperatorsView: View {
@State private var showOperatorsSheet = false
var body: some View {
VStack(alignment: .leading) {
Image((operatorsInfo[.flux] ?? ServerOperator.dummyOperatorInfo).largeLogo)
.resizable()
.scaledToFit()
.frame(height: 48)
Text("The second preset operator in the app!")
.multilineTextAlignment(.leading)
.lineLimit(10)
HStack {
Button("Enable Flux") {
showOperatorsSheet = true
}
Text("for better metadata privacy.")
}
}
.sheet(isPresented: $showOperatorsSheet) {
NavigationView {
ChooseServerOperators(onboarding: false)
.modifier(ThemedBackground())
}
}
}
}
private enum WhatsNewViewSheet: Identifiable {
case showConditions
var id: String {
switch self {
case .showConditions: return "showConditions"
}
}
}
struct WhatsNewView: View {
@Environment(\.dismiss) var dismiss: DismissAction
@EnvironmentObject var theme: AppTheme
@State var currentVersion = versionDescriptions.count - 1
@State var currentVersionNav = versionDescriptions.count - 1
var viaSettings = false
var updatedConditions: Bool
@State private var sheetItem: WhatsNewViewSheet? = nil
var body: some View {
whatsNewView()
.sheet(item: $sheetItem) { item in
switch item {
case .showConditions:
UsageConditionsView(
currUserServers: Binding.constant([]),
userServers: Binding.constant([])
)
.modifier(ThemedBackground(grouped: true))
}
}
}
private func whatsNewView() -> some View {
VStack {
TabView(selection: $currentVersion) {
ForEach(Array(versionDescriptions.enumerated()), id: \.0) { (i, v) in
@@ -499,9 +626,11 @@ struct WhatsNewView: View {
.foregroundColor(theme.colors.secondary)
.frame(maxWidth: .infinity)
.padding(.vertical)
ForEach(v.features, id: \.title) { f in
featureDescription(f)
.padding(.bottom, 8)
ForEach(v.features) { f in
switch f {
case let .feature(d): featureDescription(d).padding(.bottom, 8)
case let .view(v): AnyView(v.view()).padding(.bottom, 8)
}
}
if let post = v.post {
Link(destination: post) {
@@ -511,13 +640,20 @@ struct WhatsNewView: View {
}
}
}
if updatedConditions {
Button("View updated conditions") {
sheetItem = .showConditions
}
}
if !viaSettings {
Spacer()
Button("Ok") {
dismiss()
}
.font(.title3)
.frame(maxWidth: .infinity, alignment: .center)
Spacer()
}
}
@@ -535,20 +671,24 @@ struct WhatsNewView: View {
currentVersionNav = currentVersion
}
}
private func featureDescription(_ f: FeatureDescription) -> some View {
VStack(alignment: .leading, spacing: 4) {
if let icon = f.icon {
HStack(alignment: .center, spacing: 4) {
Image(systemName: icon)
.symbolRenderingMode(.monochrome)
.foregroundColor(theme.colors.secondary)
.frame(minWidth: 30, alignment: .center)
Text(f.title).font(.title3).bold()
}
} else {
Text(f.title).font(.title3).bold()
@ViewBuilder private func featureHeader(_ icon: String?, _ title: LocalizedStringKey) -> some View {
if let icon {
HStack(alignment: .center, spacing: 4) {
Image(systemName: icon)
.symbolRenderingMode(.monochrome)
.foregroundColor(theme.colors.secondary)
.frame(minWidth: 30, alignment: .center)
Text(title).font(.title3).bold()
}
} else {
Text(title).font(.title3).bold()
}
}
private func featureDescription(_ f: Description) -> some View {
VStack(alignment: .leading, spacing: 4) {
featureHeader(f.icon, f.title)
if let d = f.description {
Text(d)
.multilineTextAlignment(.leading)
@@ -603,6 +743,6 @@ struct WhatsNewView: View {
struct NewFeaturesView_Previews: PreviewProvider {
static var previews: some View {
WhatsNewView()
WhatsNewView(updatedConditions: false)
}
}
@@ -14,7 +14,6 @@ struct ConnectDesktopView: View {
@EnvironmentObject var m: ChatModel
@EnvironmentObject var theme: AppTheme
@Environment(\.dismiss) var dismiss: DismissAction
var viaSettings = false
@AppStorage(DEFAULT_DEVICE_NAME_FOR_REMOTE_ACCESS) private var deviceName = UIDevice.current.name
@AppStorage(DEFAULT_CONFIRM_REMOTE_SESSIONS) private var confirmRemoteSessions = false
@AppStorage(DEFAULT_CONNECT_REMOTE_VIA_MULTICAST) private var connectRemoteViaMulticast = true
@@ -57,23 +56,6 @@ struct ConnectDesktopView: View {
}
var body: some View {
if viaSettings {
viewBody
.modifier(BackButton(label: "Back", disabled: Binding.constant(false)) {
if m.activeRemoteCtrl {
alert = .disconnectDesktop(action: .back)
} else {
dismiss()
}
})
} else {
NavigationView {
viewBody
}
}
}
var viewBody: some View {
Group {
let discovery = m.remoteCtrlSession?.discovery
if discovery == true || (discovery == nil && !showConnectScreen) {
@@ -286,7 +268,7 @@ struct ConnectDesktopView: View {
private func ctrlDeviceNameText(_ session: RemoteCtrlSession, _ rc: RemoteCtrlInfo?) -> Text {
var t = Text(rc?.deviceViewName ?? session.ctrlAppInfo?.deviceName ?? "")
if (rc == nil) {
t = t + Text(" ") + Text("(new)").italic()
t = t + textSpace + Text("(new)").italic()
}
return t
}
@@ -295,7 +277,7 @@ struct ConnectDesktopView: View {
let v = session.ctrlAppInfo?.appVersionRange.maxVersion
var t = Text("v\(v ?? "")")
if v != session.appVersion {
t = t + Text(" ") + Text("(this device v\(session.appVersion))").italic()
t = t + textSpace + Text("(this device v\(session.appVersion))").italic()
}
return t
}
+6 -1
View File
@@ -105,7 +105,12 @@ struct TerminalView: View {
}
}
.navigationViewStyle(.stack)
.navigationTitle("Chat console")
.toolbar {
// Redaction broken for `.navigationTitle` - using a toolbar item instead.
ToolbarItem(placement: .principal) {
Text("Chat console").font(.headline)
}
}
.modifier(ThemedBackground())
}
@@ -19,9 +19,15 @@ extension AppSettings {
val.hostMode = .publicHost
val.requiredHostMode = true
}
val.socksProxy = nil
setNetCfg(val)
if val.socksProxy != nil {
val.socksProxy = networkProxy?.toProxyString()
setNetCfg(val, networkProxy: networkProxy)
} else {
val.socksProxy = nil
setNetCfg(val, networkProxy: nil)
}
}
if let val = networkProxy { networkProxyDefault.set(val) }
if let val = privacyEncryptLocalFiles { privacyEncryptLocalFilesGroupDefault.set(val) }
if let val = privacyAskToApproveRelays { privacyAskToApproveRelaysGroupDefault.set(val) }
if let val = privacyAcceptImages {
@@ -52,17 +58,21 @@ extension AppSettings {
profileImageCornerRadiusGroupDefault.set(val)
def.setValue(val, forKey: DEFAULT_PROFILE_IMAGE_CORNER_RADIUS)
}
if let val = uiColorScheme { def.setValue(val, forKey: DEFAULT_CURRENT_THEME) }
if let val = uiDarkColorScheme { def.setValue(val, forKey: DEFAULT_SYSTEM_DARK_THEME) }
if let val = uiCurrentThemeIds { def.setValue(val, forKey: DEFAULT_CURRENT_THEME_IDS) }
if let val = uiThemes { def.setValue(val.skipDuplicates(), forKey: DEFAULT_THEME_OVERRIDES) }
if let val = uiChatItemRoundness { def.setValue(val, forKey: DEFAULT_CHAT_ITEM_ROUNDNESS)}
if let val = uiChatItemTail { def.setValue(val, forKey: DEFAULT_CHAT_ITEM_TAIL)}
if let val = uiColorScheme { currentThemeDefault.set(val) }
if let val = uiDarkColorScheme { systemDarkThemeDefault.set(val) }
if let val = uiCurrentThemeIds { currentThemeIdsDefault.set(val) }
if let val = uiThemes { themeOverridesDefault.set(val.skipDuplicates()) }
if let val = oneHandUI { groupDefaults.setValue(val, forKey: GROUP_DEFAULT_ONE_HAND_UI) }
if let val = chatBottomBar { groupDefaults.setValue(val, forKey: GROUP_DEFAULT_CHAT_BOTTOM_BAR) }
}
public static var current: AppSettings {
let def = UserDefaults.standard
var c = AppSettings.defaults
c.networkConfig = getNetCfg()
c.networkProxy = networkProxyDefault.get()
c.privacyEncryptLocalFiles = privacyEncryptLocalFilesGroupDefault.get()
c.privacyAskToApproveRelays = privacyAskToApproveRelaysGroupDefault.get()
c.privacyAcceptImages = privacyAcceptImagesGroupDefault.get()
@@ -84,11 +94,14 @@ extension AppSettings {
c.iosCallKitEnabled = callKitEnabledGroupDefault.get()
c.iosCallKitCallsInRecents = def.bool(forKey: DEFAULT_CALL_KIT_CALLS_IN_RECENTS)
c.uiProfileImageCornerRadius = def.double(forKey: DEFAULT_PROFILE_IMAGE_CORNER_RADIUS)
c.uiChatItemRoundness = def.double(forKey: DEFAULT_CHAT_ITEM_ROUNDNESS)
c.uiChatItemTail = def.bool(forKey: DEFAULT_CHAT_ITEM_TAIL)
c.uiColorScheme = currentThemeDefault.get()
c.uiDarkColorScheme = systemDarkThemeDefault.get()
c.uiCurrentThemeIds = currentThemeIdsDefault.get()
c.uiThemes = themeOverridesDefault.get()
c.oneHandUI = groupDefaults.bool(forKey: GROUP_DEFAULT_ONE_HAND_UI)
c.chatBottomBar = groupDefaults.bool(forKey: GROUP_DEFAULT_CHAT_BOTTOM_BAR)
return c
}
}

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