Compare commits

...

550 Commits

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

* Update 20220808-simplex-chat-v3.1-chat-groups.md
2023-04-30 08:54:21 +01:00
lastimeoceanisfall 7159195cb9 Create 20220901-simplex-chat-v3.2-incognito-mode.md (#2295) 2023-04-30 08:54:04 +01:00
lastimeoceanisfall 45d3855d84 Create 20220928-simplex-chat-v4-encrypted-database.md (#2296) 2023-04-30 08:53:48 +01:00
lastimeoceanisfall 650a6f0758 Create 20221108-simplex-chat-v4.2-security-audit-new-website.md (#2297) 2023-04-30 08:53:25 +01:00
lastimeoceanisfall ae6ba0cfb5 Create 20221206-simplex-chat-v4.3-voice-messages.md (#2298) 2023-04-30 08:53:03 +01:00
lastimeoceanisfall d9a2317f82 Create 20230103-simplex-chat-v4.4-disappearing-messages.md (#2299) 2023-04-30 08:52:47 +01:00
lastimeoceanisfall 3d8b521c0c Create 20230204-simplex-chat-v4-5-user-chat-profiles.md (#2300) 2023-04-30 08:52:28 +01:00
lastimeoceanisfall 1fd5bbbc4f Create 20230301-simplex-file-transfer-protocol.md (#2301) 2023-04-30 08:52:07 +01:00
lastimeoceanisfall 62299cbf0b Create 20230328-simplex-chat-v4-6-hidden-profiles.md (#2302) 2023-04-30 08:51:47 +01:00
lastimeoceanisfall 3eb969d3c5 Create 20230422-simplex-chat-vision-funding-v5-videos-files-passcode.md (#2303) 2023-04-30 08:51:09 +01:00
lastimeoceanisfall 85dba0f36b Create README.md (#2304) 2023-04-30 08:50:39 +01:00
lastimeoceanisfall cb6490ed59 Create ANDROID.md (#2240) 2023-04-30 08:50:18 +01:00
Evgeny Poberezkin 315d830357 android: export all theme colors (#2348) 2023-04-29 13:11:44 +01:00
M Sarmad Qadeer 00caeae914 website: add simplex reviews section (#2346)
* website: add simplex reviews section

* website: update review section position & add quality images

* website: add dark mode images

* website: avoid text selection when hover on logos

* website: add layout fix

* website: add spaces in simplex review

* website: improve margin of simplex review

* website: add title to logos in simplex review

* titles, order

* update images

* move images

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-04-29 09:53:27 +01:00
Stanislav Dmitrenko 607f77d432 android: more checks for colors in customized theme (#2343)
* android: more checks for colors in customized theme

* code style

* refactor

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-04-28 09:09:42 +01:00
Stanislav Dmitrenko c254b33753 android: blue theme and more options (#2331)
* android: blue theme and more options

* more color options and better code

* more color options and some fixes

* removed preferences about non-existent theme

* colors

* Revert "removed preferences about non-existent theme"

This reverts commit cbb38d54a8.

* colors

* update colors

* migrations

* HighOrLowLight -> secondary

* new color

* color

* update colors, move colors to a separate page

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-04-27 20:32:40 +01:00
Stanislav Dmitrenko 59f3848056 android: cancel and destructive buttons in alerts (#2238) 2023-04-27 17:00:03 +01:00
Stanislav Dmitrenko 69aa002c83 android: prevent click & long click at the same time (#2237) 2023-04-27 16:44:12 +01:00
sh 8630d1ab12 templates: update labels (#2335) 2023-04-27 09:18:40 +01:00
Evgeny Poberezkin 591aa9eaa5 core: get all chat items API (#2333)
* core: get all chat items API

* test
2023-04-27 08:12:34 +01:00
sh f82fa42cba github: issue templates (#2330)
* issue template test

* templates: allow blank issues
2023-04-26 16:37:13 +01:00
Evgeny Poberezkin aa441c88db Merge branch 'stable' 2023-04-26 08:30:55 +01:00
Evgeny Poberezkin 17ee22da72 readme: update group link 2023-04-26 08:30:35 +01:00
spaced4ndy a9957fb46d core: delete xftp file when user is not found by file id (#2234) 2023-04-25 15:46:00 +04:00
Stanislav Dmitrenko f5c87fdd4c android: better auth when opening profiles (#2236)
* android: better auth when opening profiles

* consistent behaviour between auth methods

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-04-25 10:45:46 +01:00
Stanislav Dmitrenko 23467a2248 android: lint fix and removed unused lib (#2235) 2023-04-25 10:31:26 +01:00
M Sarmad Qadeer 9fa93e40cb website: add atom feeds for blogs (#2233)
* website: add atom feeds for blogs

* update front-matter

* website: add atom and rss feeds for blogs

* include full blog content

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-04-24 19:31:59 +01:00
M Sarmad Qadeer f4b852d2dd website: fix chines in dropdown (#2232) 2023-04-24 18:54:57 +01:00
Stanislav Dmitrenko f698b7fa9f android: new icons (#2231)
* android: new icons

* different size

* all icons were changed

* sizes

* account icon was returned

* icon

* two icons

* bolt icon

* fix change avatar

* more vert button

* changes in icons

* icon size

* deleted unneeded icon

* icons

* spacer and padding

* no comments

* height of item

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-04-24 18:51:21 +01:00
Stanislav Dmitrenko b21afa648d android: UserPicker enhancements (#2230)
* android: UserPicker enhancements

* paddings
2023-04-23 10:42:06 +01:00
Evgeny Poberezkin b9575cc869 website: fix qrcode dependency 2023-04-22 17:19:49 +01:00
Evgeny Poberezkin 3ea91bc4ad Merge branch 'stable' 2023-04-22 17:04:02 +01:00
Evgeny Poberezkin b3dce5fdb0 blog: v5 announcement (#2225)
* blog: vision, funding, v5 announcement

* keep file name

* update

* update blog

* images, site preview

* fix link

* remove duplication

* corrections
2023-04-22 17:03:41 +01:00
Evgeny Poberezkin 28ad8b8cd5 blog: v5 announcement (#2225)
* blog: vision, funding, v5 announcement

* keep file name

* update

* update blog

* images, site preview

* fix link

* remove duplication

* corrections
2023-04-22 17:01:52 +01:00
Stanislav Dmitrenko 37d4ef770c android: equal paddings between sections and bottom spacer (#2227)
* android: equal paddings between sections and bottom spacer

* one more

* aligning

* paddings

* paddings

* scream color

* switch

* background and scrim colors

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-04-22 16:38:32 +01:00
Stanislav Dmitrenko ba24e40512 android: settings refactoring and new design (#2226)
* android: settings refactoring and new design

* spacers

* paddings

* paddings

* padding

* weight

* new chat button padding

* removed background color

* profiles

* cancel button function
2023-04-21 20:21:44 +01:00
Evgeny Poberezkin 8097593f5e website: anchor to join simplex 2023-04-21 13:42:44 +01:00
sh 23ccd69b5e docs: add xftp (#2223)
* docs: add xftp

* xftp: clarify statistics section

* xftp: expand configuring the app section
2023-04-21 11:33:55 +01:00
spaced4ndy 5e0d6d77b9 core: check max file size before sending (#2224) 2023-04-21 13:46:56 +04:00
spaced4ndy a06393f520 core: file status command for XFTP files (#2222) 2023-04-21 13:36:44 +04:00
Evgeny Poberezkin 549ffcefc0 blog: v5 announcement placeholder 2023-04-20 19:57:06 +01:00
spaced4ndy c8721e8000 5.0: Android 117, iOS 144 2023-04-20 20:26:12 +04:00
Evgeny Poberezkin 03882367da core: 5.0.0.2 2023-04-20 14:19:09 +01:00
spaced4ndy 4d700d113d core, ios: mark files to receive from NSE, receive marked files on chat start (#2218) 2023-04-20 16:52:55 +04:00
Stanislav Dmitrenko 17bdd2a1d2 android: different icons for attachments (#2219)
* android: different icons for attachments

* icon

* change color

* changes

* strings

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-04-20 13:27:27 +01:00
Evgeny Poberezkin 80a68012a2 website: translations (#2217)
* Added translation using Weblate (Polish)

* Added translation using Weblate (Polish)

* Translated using Weblate (Polish)

Currently translated at 33.1% (70 of 211 strings)

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

---------

Co-authored-by: Display Name <ptrumtine@proton.me>
2023-04-20 11:56:53 +01:00
Evgeny Poberezkin 3742906f75 mobile: translations (#2216)
* Translated using Weblate (Spanish)

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

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

* Translated using Weblate (Czech)

Currently translated at 99.8% (1123 of 1125 strings)

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

* Translated using Weblate (Czech)

Currently translated at 100.0% (1043 of 1043 strings)

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

* Translated using Weblate (French)

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

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

* Translated using Weblate (Italian)

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

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (1125 of 1125 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (1125 of 1125 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 98.7% (1029 of 1042 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (1125 of 1125 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (1042 of 1042 strings)

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

* Translated using Weblate (Spanish)

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

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

* Translated using Weblate (Czech)

Currently translated at 99.8% (1123 of 1125 strings)

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

* Translated using Weblate (Czech)

Currently translated at 100.0% (1043 of 1043 strings)

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

* Translated using Weblate (French)

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

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

* Translated using Weblate (Italian)

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

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (1125 of 1125 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (1125 of 1125 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 98.7% (1029 of 1042 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (1125 of 1125 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (1042 of 1042 strings)

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

* Translated using Weblate (German)

Currently translated at 100.0% (1125 of 1125 strings)

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

* Translated using Weblate (Polish)

Currently translated at 100.0% (1125 of 1125 strings)

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

* Translated using Weblate (German)

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

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

* Translated using Weblate (French)

Currently translated at 100.0% (1125 of 1125 strings)

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (1125 of 1125 strings)

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

* Translated using Weblate (Chinese (Simplified))

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

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

* ios: import/export localizations

---------

Co-authored-by: No name <CertainBot@users.noreply.hosted.weblate.org>
Co-authored-by: zenobit <zen@osowoso.xyz>
Co-authored-by: Ophiushi <41908476+ishi-sama@users.noreply.github.com>
Co-authored-by: random r <epsilin@yopmail.com>
Co-authored-by: Float <float.hu+@gmail.com>
Co-authored-by: sith-on-mars <groguko36@pm.me>
Co-authored-by: mlanp <github@lang.xyz>
Co-authored-by: B.O.S.S <BxOxSxS@protonmail.com>
2023-04-20 11:55:04 +01:00
Stanislav Dmitrenko ae90edcdb5 android: moved to BasicTextField in some places (#2215)
* adnroid: moved to BasicTextField in some places

* field height

* field height 2

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-04-20 11:29:50 +01:00
Evgeny Poberezkin 9e76aadb0f Merge branch 'stable' 2023-04-19 18:09:57 +01:00
Evgeny Poberezkin 043544d7ec readme: new users group 2023-04-19 18:09:05 +01:00
Evgeny Poberezkin 2bf7d1dddc android: share video as video (#2214) 2023-04-19 17:39:31 +01:00
Evgeny Poberezkin e1741118ce android: fix profile string (#2213) 2023-04-19 15:52:33 +01:00
spaced4ndy 58fb3f7f2d android: fix onboarding fonts, paddings (#2212) 2023-04-19 18:48:00 +04:00
spaced4ndy 48e92a7e9b android: don't show button to hide user if authentication is not configured (#2211) 2023-04-19 18:16:26 +04:00
spaced4ndy 5bf16da09d core, mobile: prohibit to change chat item expiration when chat is stopped (#2210) 2023-04-19 15:21:28 +04:00
Evgeny Poberezkin 23ca3dd665 5.0.0-beta.2 2023-04-19 11:59:39 +01:00
Evgeny Poberezkin 2caff25fa2 android: revert to using gallery for images and videos (#2209)
* core: revert to using gallery for images and videos

* remove comment
2023-04-19 11:07:14 +01:00
Evgeny Poberezkin 37f835be8c mobile: remove XFTP toggle (#2208)
* mobile: remove XFTP toggle

* ios: remove unused string

* android: remove unused strings
2023-04-19 09:41:01 +01:00
Evgeny Poberezkin a6c1f2f776 5.0.0-beta.1: Android 115, iOS 142 2023-04-19 00:47:34 +01:00
Stanislav Dmitrenko acf0dfc38c android: alerts + dropdown menus (#2199)
* android: alerts + dropdown menus

* colors

* align

* paddings

* colors

* complete alerts

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-04-19 00:17:28 +01:00
Evgeny Poberezkin 67f13831ce mobile: translations (#2207)
* Translated using Weblate (German)

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

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

* Translated using Weblate (German)

Currently translated at 97.6% (1018 of 1043 strings)

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

* Translated using Weblate (Russian)

Currently translated at 100.0% (1043 of 1043 strings)

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

* Translated using Weblate (French)

Currently translated at 99.2% (1035 of 1043 strings)

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

* Translated using Weblate (Italian)

Currently translated at 99.2% (1035 of 1043 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 97.6% (1018 of 1043 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 97.6% (1018 of 1043 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% (1125 of 1125 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% (1043 of 1043 strings)

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

* Translated using Weblate (Dutch)

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

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

* Translated using Weblate (Polish)

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

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

* Translated using Weblate (Polish)

Currently translated at 100.0% (1043 of 1043 strings)

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

* ios: import/export localizations

---------

Co-authored-by: mlanp <github@lang.xyz>
Co-authored-by: John m <jvanmanen@gmail.com>
Co-authored-by: B.O.S.S <BxOxSxS@protonmail.com>
2023-04-18 21:30:18 +01:00
Evgeny Poberezkin d50e65af20 core: 5.0.0.1 2023-04-18 21:04:34 +01:00
Evgeny Poberezkin b8d74b74a6 website: translations (#2206)
* Translated using Weblate (Arabic)

Currently translated at 100.0% (211 of 211 strings)

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

* Translated using Weblate (Portuguese (Brazil))

Currently translated at 3.3% (7 of 211 strings)

Translation: SimpleX Chat/SimpleX Chat website
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/website/pt_BR/
2023-04-18 18:52:48 +01:00
Evgeny Poberezkin 802a28d759 mobile: translations (#2204)
* Translated using Weblate (Italian)

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

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

* Translated using Weblate (Czech)

Currently translated at 99.8% (1089 of 1091 strings)

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

* Translated using Weblate (Polish)

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

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

* Translated using Weblate (German)

Currently translated at 95.6% (1044 of 1091 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (1091 of 1091 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (1013 of 1013 strings)

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

* Translated using Weblate (Spanish)

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

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

* Translated using Weblate (Korean)

Currently translated at 78.5% (857 of 1091 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 98.8% (1090 of 1103 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% (1013 of 1013 strings)

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

* Translated using Weblate (German)

Currently translated at 98.0% (1082 of 1103 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (1103 of 1103 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.8% (1090 of 1103 strings)

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

* Translated using Weblate (Polish)

Currently translated at 100.0% (1103 of 1103 strings)

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

* Translated using Weblate (German)

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

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

* Translated using Weblate (Czech)

Currently translated at 100.0% (1101 of 1101 strings)

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

* Translated using Weblate (Czech)

Currently translated at 100.0% (1013 of 1013 strings)

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

* Translated using Weblate (Hindi)

Currently translated at 16.2% (179 of 1101 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 100.0% (1101 of 1101 strings)

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

* Translated using Weblate (Chinese (Simplified))

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

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

* Translated using Weblate (Hindi)

Currently translated at 16.3% (180 of 1102 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 100.0% (1119 of 1119 strings)

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

* Translated using Weblate (French)

Currently translated at 99.6% (1115 of 1119 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% (1013 of 1013 strings)

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (1119 of 1119 strings)

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

* Translated using Weblate (Polish)

Currently translated at 99.2% (1111 of 1119 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (1119 of 1119 strings)

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

* Translated using Weblate (Polish)

Currently translated at 100.0% (1119 of 1119 strings)

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

* ios: import localizations

* ios: re-export localizations

---------

Co-authored-by: random r <epsilin@yopmail.com>
Co-authored-by: No name <CertainBot@users.noreply.hosted.weblate.org>
Co-authored-by: John m <jvanmanen@gmail.com>
Co-authored-by: B.O.S.S <BxOxSxS@protonmail.com>
Co-authored-by: zenobit <zen@osowoso.xyz>
Co-authored-by: sith-on-mars <groguko36@pm.me>
Co-authored-by: mf <work.j6nnu@slmail.me>
Co-authored-by: 5olivetree <5olivetree+github.com@mailbox.org>
Co-authored-by: mlanp <github@lang.xyz>
Co-authored-by: Ram <airavatam@tutanota.com>
Co-authored-by: Ophiushi <41908476+ishi-sama@users.noreply.github.com>
2023-04-18 18:41:49 +01:00
Evgeny Poberezkin c85d43b349 ios: export localizations 2023-04-18 16:59:51 +01:00
spaced4ndy 64ad491197 mobile: what's new in 5.0 (#2200)
* mobile: what's new in 5.0

* update texts

* update

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-04-18 16:54:56 +01:00
spaced4ndy ddd8e719ef core, ios: don't send/receive XFTP files in NSE (#2202) 2023-04-18 19:43:16 +04:00
spaced4ndy e51f7a51cc core: prohibit SMP file handshake for XFTP files on sender side, except for images and voice messages (#2201) 2023-04-18 18:51:14 +04:00
Stanislav Dmitrenko e66f5d488b android: more enhancements to layouts (#2196)
* android: more enhancements to layouts

* changes

* unused code

* unused code
2023-04-18 11:11:00 +01:00
spaced4ndy b386cc83a7 core, mobile: enable xftp by default (#2198) 2023-04-18 13:49:09 +04:00
spaced4ndy 09481e09b6 core, mobile: file error statuses, cancel sent file (#2193) 2023-04-18 12:48:36 +04:00
Evgeny Poberezkin 6913bf1a46 mobile: Calls chat preference (#2195)
* ios: add calls to chat preferences

* android: types and strings for Calls preference

* android: UI for Calls preference
2023-04-18 09:29:49 +01:00
Stanislav Dmitrenko e6c87ff00b android: profiles UI (#2194)
* android: profiles UI

* reverted color change

* changes

* update color, padding

* colors changes and error check

* focused field color

* refactor

* focus

* layout

* profile editor

* centered layout

* bottom paddings

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-04-17 22:10:42 +01:00
Evgeny Poberezkin 5f41cf3c52 core: change default for Disappearing Messages to "allow", mobile: support disabling without prohibiting (#2192)
* core: change default for Disappearing Messages to "allow", mobile: support disabling without prohibiting

* fix tests

* disable tests back in CI

* fixed tests 2

* remove enable

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

---------

Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
2023-04-17 18:13:10 +01:00
spaced4ndy 9edbe2e589 core: fix sending file to empty group (#2191) 2023-04-17 15:33:15 +04:00
Evgeny Poberezkin b6876712f0 core: chat preference for audio/video calls (#2188)
* core: chat preference for audio/video calls

* correction

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

* clean up

---------

Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
2023-04-17 10:18:04 +01:00
Evgeny Poberezkin 5b4c183466 core: do not create decryption error chat items for earlier messages (#2189)
* core: do not create decryption error chat items for earlier messages

* do not report earlier error, mobile items, fix tests
2023-04-16 18:30:25 +01:00
Evgeny Poberezkin 393238f47c android: change UInt type 2023-04-16 15:56:12 +01:00
Evgeny Poberezkin aea526f69d core: add chat items to indicate decryption failures due to ratchets being out of sync (#2175)
* core: add chat items to indicate decryption failures due to ratchets being out of sync

* show ratchet errors in chat items, show all integrity errors

* show decryption errors, tests

* ios: chat items, remove item for duplicate messages

* android: decryption errors chat items

* eol
2023-04-16 11:35:45 +01:00
Evgeny Poberezkin 13cf1cc004 ios: mute groups instantly when notifications are disabled (#2187) 2023-04-14 22:54:05 +01:00
Evgeny Poberezkin 1766a8bb2c 5.0.0-beta.0: Android 114 2023-04-14 20:01:08 +01:00
spaced4ndy 9a5732ab70 ios: 5.0 (141) 2023-04-14 20:43:45 +04:00
Evgeny Poberezkin 2923ca1356 ios: fix profile deletion on iOS 15 (#2185) 2023-04-14 16:51:55 +01:00
spaced4ndy 96f0083384 core: 5.0.0.0 2023-04-14 18:05:16 +04:00
Evgeny Poberezkin febfc396e3 ios: UI to cancel receiving file (#2123)
* ios: UI to cancel receiving file

* different alert
2023-04-14 13:57:17 +01:00
spaced4ndy 29735a807b core: don't delete XFTP file when temporary agent error is reported in RFERR/SFERR (#2184) 2023-04-14 15:52:39 +04:00
spaced4ndy eb36f64676 core: update simplexmq (digest entity id); integrate xftp snd delete (#2183) 2023-04-14 15:32:12 +04:00
Evgeny Poberezkin 4e01970d69 core: remove build timestamp from the version info (reproducible builds) (#2182)
* core: remove build timestamp from the version info (reproducible builds)

* remove strings
2023-04-14 12:03:41 +01:00
Evgeny Poberezkin e5713087e3 ios: export Polish localizations 2023-04-14 11:26:13 +01:00
Evgeny Poberezkin b40fc7ff18 mobile: add Polish language (#2181)
* mobile: add Polish language

* update readme
2023-04-14 10:20:58 +01:00
Evgeny Poberezkin 06ad2b7972 website: translations (#2180)
* ios: UI to cancel receiving file

* Translated using Weblate (Portuguese (Brazil))

Currently translated at 1.8% (4 of 211 strings)

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

* Revert "ios: UI to cancel receiving file"

This reverts commit 0fcae6e8d5.

---------

Co-authored-by: Pedro Licio <amaralrj@gmail.com>
2023-04-13 23:15:18 +01:00
Evgeny Poberezkin 14eeb4451c mobile: translations (#2179)
* Translated using Weblate (French)

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

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

* Translated using Weblate (Italian)

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

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

* Translated using Weblate (Chinese (Simplified))

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

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

* Translated using Weblate (Korean)

Currently translated at 71.7% (762 of 1062 strings)

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

* Translated using Weblate (Polish)

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

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (1062 of 1062 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (968 of 968 strings)

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

* Translated using Weblate (Spanish)

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

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

* Translated using Weblate (French)

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

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

* Translated using Weblate (Italian)

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

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

* Translated using Weblate (Chinese (Simplified))

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

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

* Translated using Weblate (Korean)

Currently translated at 71.7% (762 of 1062 strings)

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

* Translated using Weblate (Polish)

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

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (1062 of 1062 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (968 of 968 strings)

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

* Translated using Weblate (Spanish)

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

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

* Translated using Weblate (French)

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

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (1014 of 1014 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (1062 of 1062 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (1014 of 1014 strings)

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

* Translated using Weblate (Spanish)

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

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

* Translated using Weblate (Hindi)

Currently translated at 16.8% (179 of 1062 strings)

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

* Translated using Weblate (Korean)

Currently translated at 80.1% (851 of 1062 strings)

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

* Translated using Weblate (Polish)

Currently translated at 100.0% (1014 of 1014 strings)

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

* ios: import/export localizations

---------

Co-authored-by: Ophiushi <41908476+ishi-sama@users.noreply.github.com>
Co-authored-by: random r <epsilin@yopmail.com>
Co-authored-by: Float <float.hu+@gmail.com>
Co-authored-by: John m <jvanmanen@gmail.com>
Co-authored-by: 5olivetree <5olivetree+github.com@mailbox.org>
Co-authored-by: B.O.S.S <BxOxSxS@protonmail.com>
Co-authored-by: sith-on-mars <groguko36@pm.me>
Co-authored-by: No name <CertainBot@users.noreply.hosted.weblate.org>
Co-authored-by: Ram <airavatam@tutanota.com>
2023-04-13 23:11:36 +01:00
Stanislav Dmitrenko ead67adeb8 android: passcode implementation (#2177)
* android: passcode implementation

* layout

* passcode view

* unused param

* text for auth

* small changes

* fix

* use preference instead of toggle

* removed useless code and changed title of auth screen

* removed unneeded function

* EOLs

* changed local variable logic to global variable

* formatting

* different alert

* changed code placement

* alert behaviour

* button size

* tint of buttons

* error instead of failed status

* do not show auth alerts on failures, only on final errors

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-04-13 22:09:41 +01:00
spaced4ndy c2d70a5107 core: update simplexmq (recoverable xftp send) (#2178) 2023-04-13 20:46:07 +04:00
spaced4ndy 90dffc975a core: request and save extra recipient file descriptions (#2170) 2023-04-12 14:47:54 +04:00
Evgeny Poberezkin e5ba7caddc ios: export localizations 2023-04-12 11:39:45 +01:00
Evgeny Poberezkin ec6cee1389 ios: digital password (instead of device auth) (#2169)
* ios: digital password (instead of device auth)

* set, ask, change password

* kind of working, sometimes

* ZSTack

* fix cancel

* update title

* fix password showing after settings dismissed

* disable button when 16 digits entered

* fixes

* layout on larger screens

* do not disable auth when switching to system if system auth failed, refactor

* fix enabling auth via the initial alert

* support landscape orientation
2023-04-12 11:22:55 +01:00
Float-hu 1d16a19373 readme: translation contributors (#2172)
* Update README.md

* change

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-04-11 20:17:41 +01:00
Evgeny Poberezkin 04c90b4f07 android: SOCKS proxy settings (#2158)
* android: draft UI for SOCKS proxy settings

* footer comment

* UI for proxy host-port selection

* keyboard type in port text field

* footer

* better text validation logic

* use italic in footer

---------

Co-authored-by: Avently <7953703+avently@users.noreply.github.com>
2023-04-10 20:01:14 +01:00
Stanislav Dmitrenko 747cbb8e09 android: media attachments from one button (#2156)
* android: media attachments from one button

* refactor durationText

* use option to enable video

* icon overlay

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-04-10 16:28:21 +01:00
Evgeny Poberezkin 834487e548 4.6.1: Android 112, iOS 140 2023-04-08 21:17:47 +01:00
Evgeny Poberezkin e1b6b54209 website: translations (#2160)
* ios: UI to cancel receiving file

* Added translation using Weblate (Portuguese (Brazil))

* ios: UI to cancel receiving file

* Added translation using Weblate (Portuguese (Brazil))

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (211 of 211 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (211 of 211 strings)

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

* Revert "ios: UI to cancel receiving file"

This reverts commit 397788cb0d.

---------

Co-authored-by: sith-on-mars <groguko36@pm.me>
Co-authored-by: Float <float.hu+@gmail.com>
2023-04-08 20:58:22 +01:00
Evgeny Poberezkin 6366d60e4c mobile: translations (#2159)
* ios: UI to cancel receiving file

* Added translation using Weblate (Polish)

* Added translation using Weblate (Polish)

* Translated using Weblate (Italian)

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

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (1044 of 1044 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (968 of 968 strings)

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

* Translated using Weblate (Czech)

Currently translated at 100.0% (968 of 968 strings)

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

* Translated using Weblate (Portuguese (Brazil))

Currently translated at 80.8% (844 of 1044 strings)

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

* Translated using Weblate (Korean)

Currently translated at 64.1% (670 of 1044 strings)

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

* Translated using Weblate (Polish)

Currently translated at 100.0% (1044 of 1044 strings)

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

* Translated using Weblate (Portuguese (Brazil))

Currently translated at 80.9% (845 of 1044 strings)

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

* Translated using Weblate (French)

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

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

* Translated using Weblate (Portuguese (Brazil))

Currently translated at 95.4% (996 of 1044 strings)

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

* Translated using Weblate (Polish)

Currently translated at 16.1% (156 of 968 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 100.0% (968 of 968 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 96.8% (1011 of 1044 strings)

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

* Translated using Weblate (Polish)

Currently translated at 100.0% (1044 of 1044 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 59.4% (575 of 968 strings)

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

* Translated using Weblate (Portuguese (Brazil))

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

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

* Translated using Weblate (Spanish)

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

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

* Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (1044 of 1044 strings)

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

* Translated using Weblate (Polish)

Currently translated at 59.5% (576 of 968 strings)

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

* Translated using Weblate (Polish)

Currently translated at 100.0% (1044 of 1044 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 80.2% (777 of 968 strings)

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

* Translated using Weblate (Polish)

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

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

* Translated using Weblate (German)

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

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

* Translated using Weblate (Lithuanian)

Currently translated at 6.8% (71 of 1044 strings)

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

* Translated using Weblate (German)

Currently translated at 100.0% (968 of 968 strings)

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

* Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (1044 of 1044 strings)

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

* Translated using Weblate (Chinese (Traditional))

Currently translated at 38.5% (373 of 968 strings)

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

* Translated using Weblate (Lithuanian)

Currently translated at 23.7% (248 of 1044 strings)

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

* Translated using Weblate (Korean)

Currently translated at 0.1% (1 of 968 strings)

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

* Translated using Weblate (Polish)

Currently translated at 100.0% (1044 of 1044 strings)

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

* Translated using Weblate (Lithuanian)

Currently translated at 28.7% (300 of 1044 strings)

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

* Translated using Weblate (Lithuanian)

Currently translated at 34.2% (358 of 1044 strings)

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

* Translated using Weblate (Korean)

Currently translated at 69.7% (728 of 1044 strings)

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

* revert code change

* import/export localizations

---------

Co-authored-by: random r <epsilin@yopmail.com>
Co-authored-by: sith-on-mars <groguko36@pm.me>
Co-authored-by: zenobit <zen@osowoso.xyz>
Co-authored-by: mf <work.j6nnu@slmail.me>
Co-authored-by: 5olivetree <5olivetree+github.com@mailbox.org>
Co-authored-by: B.O.S.S <BxOxSxS@protonmail.com>
Co-authored-by: Ophiushi <41908476+ishi-sama@users.noreply.github.com>
Co-authored-by: John m <jvanmanen@gmail.com>
Co-authored-by: No name <CertainBot@users.noreply.hosted.weblate.org>
Co-authored-by: mlanp <github@lang.xyz>
Co-authored-by: Moo <hazap@hotmail.com>
Co-authored-by: Olivia Ng <uloo592@gmail.com>
Co-authored-by: Twoomatch <twoomatch0@xmail.re>
2023-04-08 20:55:15 +01:00
Evgeny Poberezkin fabea36682 ios: update library 2023-04-08 19:53:33 +01:00
Stanislav Dmitrenko 991332a809 android: servers UI/API (#2155)
* android: servers UI/API

* non-optional server protocol in parsed address

* make another enum for ServerProtocol

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-04-07 18:19:44 +01:00
Evgeny Poberezkin 85537a99e8 core: 4.6.1.2 2023-04-07 18:18:21 +01:00
spaced4ndy 717c90c58b mobile: remove cancelled files (#2154) 2023-04-07 17:41:07 +04:00
Evgeny Poberezkin ccb52e0acd ios: validate server protocol 2023-04-06 23:25:40 +01:00
Evgeny Poberezkin d84b30c071 core: update simplemq (preset xftp servers) 2023-04-06 23:16:16 +01:00
Evgeny Poberezkin 5ae0afe1fe ios: update servers API/UI (#2149)
* ios: update servers API/UI

* fix UI

* fix
2023-04-06 22:48:32 +01:00
Evgeny Poberezkin d250e503b0 core: update simplexmq (fix file reception on 32 bit CPUs) 2023-04-06 21:06:46 +01:00
Stanislav Dmitrenko afb0ae3d03 ios: video support (#2115)
* ios: video support

* made video experience prettier

* line reordering

* fix warning

* remove playback speed

* fullscreen player

* removed unused code

* fix conflict

* setting playing status better

* thumbnail dimensions and loading indicator

* fill under video

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-04-06 18:26:48 +01:00
Evgeny Poberezkin 1a3f0bed47 core: update servers API to include XFTP servers, ios: generalize UI to manage servers (#2140)
* core: update servers API to include XFTP servers, ios: generalize UI to manage servers

* add test

* update migrations to pass tests

* fix readme

* update simplexmq
2023-04-05 21:59:12 +01:00
Stanislav Dmitrenko 1e280fb7e1 android: prevent possible race in chat items (#2148)
* android: prevent possible race in chat items

* change
2023-04-05 18:53:35 +01:00
Evgeny Poberezkin 6feac55380 core: update http2 library 2023-04-05 10:23:35 +01:00
Evgeny Poberezkin 93d8eac037 4.6.1-beta.2: Android 111, iOS 138 2023-04-04 23:48:11 +01:00
Evgeny Poberezkin a11f99be3d mobile: ignore spaces around password (#2144) 2023-04-04 21:53:25 +01:00
Evgeny Poberezkin da17639309 core: 4.6.1.1 2023-04-04 17:31:30 +01:00
Evgeny Poberezkin 10301aa742 terminal: autocomplete contacts, groups and commands (#2125)
* terminal: autocomplete contacts, groups and commands

* autocomplete for commands and member names

* update commands

* show variants

* improve

* improve

* do not show user in contacts, better state machine for tab states

* update CI runners
2023-04-04 14:58:26 +01:00
Evgeny Poberezkin 2148d50393 core: use Int64 in time calculations (#2143)
* core: use Int64 in time calculations

* remove import

* make interval Int64
2023-04-04 14:26:31 +01:00
Evgeny Poberezkin 12fb2a4ec5 ci: move to ubuntu 20/22, disable 2 tests in CI (#2142)
* ci: move to ubuntu 20/22

* skip test on mac

* skip some tests on mac CI

* skip test on CI

* skip test unconditionally

* skip on CI only
2023-04-04 13:09:07 +01:00
Stanislav Dmitrenko 8085e5b85c android: change active user after chat started (#2141) 2023-04-03 20:11:27 +01:00
Stanislav Dmitrenko 4ba310ec16 android: open direct chat simplified (#2139) 2023-04-03 19:59:50 +01:00
Stanislav Dmitrenko 865c56f400 scripts: adapted compress-and-sign-apk script to case-insensitive file systems (#2138)
Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-04-03 19:41:42 +01:00
Stanislav Dmitrenko c510e73256 android: disallow to reply on service messages (#2136) 2023-04-03 18:53:21 +01:00
spaced4ndy 73638129bc core: cancel file transfer when chat item is marked deleted (#2137) 2023-04-03 18:49:22 +04:00
spaced4ndy 1a7a79d504 core: allow repeat receive after cancel for XFTP files (#2134) 2023-04-03 16:31:18 +04:00
spaced4ndy d3268e4a72 mobile: delete XFTP files after uploading (#2133) 2023-04-03 16:31:09 +04:00
Evgeny Poberezkin 15a93014a5 core: update http2 2023-04-01 17:27:11 +01:00
Evgeny Poberezkin e7735329bc 4.6.1-beta.1: Android 110, iOS 137, update library 2023-04-01 16:04:44 +01:00
ishi_sama 3e222c68eb docs: FR Update (#2063)
* docs: FR update

* fix android.md

* fix rev dates

fix revision dates ; ANDROID.md & TRANSLATION.md rev dates added

* fix links

* update

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-04-01 14:55:25 +01:00
M Sarmad Qadeer a596bd9011 website: nav scrolling & direction issue (#2120)
* website: add dir to blog template

* website: remove scroll in mobile dropdown menu
2023-04-01 14:31:15 +01:00
Evgeny Poberezkin 21a49710a8 ios: scripts to import/export localizations 2023-04-01 14:28:12 +01:00
Evgeny Poberezkin ce6fdb2558 mobile: translations (#2121)
* Translated using Weblate (Russian)

Currently translated at 100.0% (1044 of 1044 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% (968 of 968 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 99.3% (962 of 968 strings)

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

* Translated using Weblate (French)

Currently translated at 99.3% (962 of 968 strings)

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

* Translated using Weblate (Italian)

Currently translated at 99.3% (962 of 968 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 99.2% (961 of 968 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 99.3% (962 of 968 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.3% (962 of 968 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 99.3% (962 of 968 strings)

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

* ios: export/import localizations
2023-04-01 14:27:24 +01:00
Evgeny Poberezkin 0baee848a6 mobile: translations (#2114)
* Added translation using Weblate (Korean)

* Added translation using Weblate (Korean)

* Translated using Weblate (French)

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

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

* Translated using Weblate (Dutch)

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

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

* Translated using Weblate (Czech)

Currently translated at 100.0% (940 of 940 strings)

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

* Translated using Weblate (Korean)

Currently translated at 20.8% (210 of 1008 strings)

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

* Update translation files

Updated by "Remove blank strings" hook in Weblate.

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

* Translated using Weblate (Korean)

Currently translated at 21.1% (213 of 1008 strings)

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

* Translated using Weblate (German)

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

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

* Translated using Weblate (German)

Currently translated at 100.0% (1009 of 1009 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (1009 of 1009 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (1009 of 1009 strings)

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

* Translated using Weblate (Chinese (Traditional))

Currently translated at 99.9% (1008 of 1009 strings)

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

* Translated using Weblate (Spanish)

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

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

* Translated using Weblate (Korean)

Currently translated at 27.4% (277 of 1009 strings)

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

* Translated using Weblate (Spanish)

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

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

* Translated using Weblate (Korean)

Currently translated at 31.3% (320 of 1020 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (1020 of 1020 strings)

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

* Translated using Weblate (Dutch)

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

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

* Translated using Weblate (German)

Currently translated at 100.0% (1026 of 1026 strings)

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

* Translated using Weblate (Italian)

Currently translated at 99.9% (1025 of 1026 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (1028 of 1028 strings)

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (1028 of 1028 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 98.9% (1017 of 1028 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 99.1% (1019 of 1028 strings)

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

* Translated using Weblate (German)

Currently translated at 100.0% (1028 of 1028 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (1028 of 1028 strings)

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

* Translated using Weblate (Chinese (Traditional))

Currently translated at 1.3% (13 of 940 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 99.5% (1023 of 1028 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% (1028 of 1028 strings)

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

* Translated using Weblate (Chinese (Traditional))

Currently translated at 4.1% (39 of 940 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 100.0% (1028 of 1028 strings)

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

* Translated using Weblate (Czech)

Currently translated at 100.0% (1028 of 1028 strings)

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

* Translated using Weblate (Korean)

Currently translated at 36.2% (373 of 1028 strings)

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

* Translated using Weblate (Arabic)

Currently translated at 4.5% (47 of 1033 strings)

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (1035 of 1035 strings)

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

* Translated using Weblate (Chinese (Simplified))

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

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

* Translated using Weblate (German)

Currently translated at 100.0% (1035 of 1035 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 100.0% (1035 of 1035 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (1035 of 1035 strings)

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

* Translated using Weblate (Czech)

Currently translated at 99.8% (1033 of 1035 strings)

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

* Translated using Weblate (Korean)

Currently translated at 45.0% (466 of 1035 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (1044 of 1044 strings)

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

* Translated using Weblate (German)

Currently translated at 100.0% (1044 of 1044 strings)

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

* Translated using Weblate (Korean)

Currently translated at 50.9% (532 of 1044 strings)

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

* Translated using Weblate (Korean)

Currently translated at 51.0% (533 of 1044 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 100.0% (1044 of 1044 strings)

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

* Translated using Weblate (Korean)

Currently translated at 53.6% (560 of 1044 strings)

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

* Added translation using Weblate (Korean)

* Added translation using Weblate (Korean)

* Translated using Weblate (French)

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

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

* Translated using Weblate (Dutch)

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

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

* Translated using Weblate (Czech)

Currently translated at 100.0% (940 of 940 strings)

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

* Translated using Weblate (Korean)

Currently translated at 20.8% (210 of 1008 strings)

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

* Update translation files

Updated by "Remove blank strings" hook in Weblate.

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

* Translated using Weblate (Korean)

Currently translated at 21.1% (213 of 1008 strings)

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

* Translated using Weblate (German)

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

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

* Translated using Weblate (German)

Currently translated at 100.0% (1009 of 1009 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (1009 of 1009 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (1009 of 1009 strings)

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

* Translated using Weblate (Chinese (Traditional))

Currently translated at 99.9% (1008 of 1009 strings)

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

* Translated using Weblate (Spanish)

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

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

* Translated using Weblate (Korean)

Currently translated at 27.4% (277 of 1009 strings)

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

* Translated using Weblate (Spanish)

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

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

* Translated using Weblate (Korean)

Currently translated at 31.3% (320 of 1020 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (1020 of 1020 strings)

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

* Translated using Weblate (Dutch)

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

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

* Translated using Weblate (German)

Currently translated at 100.0% (1026 of 1026 strings)

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

* Translated using Weblate (Italian)

Currently translated at 99.9% (1025 of 1026 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (1028 of 1028 strings)

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (1028 of 1028 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 98.9% (1017 of 1028 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 99.1% (1019 of 1028 strings)

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

* Translated using Weblate (German)

Currently translated at 100.0% (1028 of 1028 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (1028 of 1028 strings)

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

* Translated using Weblate (Chinese (Traditional))

Currently translated at 1.3% (13 of 940 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 99.5% (1023 of 1028 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% (1028 of 1028 strings)

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

* Translated using Weblate (Chinese (Traditional))

Currently translated at 4.1% (39 of 940 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 100.0% (1028 of 1028 strings)

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

* Translated using Weblate (Czech)

Currently translated at 100.0% (1028 of 1028 strings)

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

* Translated using Weblate (Korean)

Currently translated at 36.2% (373 of 1028 strings)

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

* Translated using Weblate (Arabic)

Currently translated at 4.5% (47 of 1033 strings)

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (1035 of 1035 strings)

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

* Translated using Weblate (Chinese (Simplified))

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

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

* Translated using Weblate (German)

Currently translated at 100.0% (1035 of 1035 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 100.0% (1035 of 1035 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (1035 of 1035 strings)

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

* Translated using Weblate (Czech)

Currently translated at 99.8% (1033 of 1035 strings)

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

* Translated using Weblate (Korean)

Currently translated at 45.0% (466 of 1035 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (1044 of 1044 strings)

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

* Translated using Weblate (German)

Currently translated at 100.0% (1044 of 1044 strings)

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

* Translated using Weblate (Korean)

Currently translated at 50.9% (532 of 1044 strings)

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

* Translated using Weblate (Korean)

Currently translated at 51.0% (533 of 1044 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 100.0% (1044 of 1044 strings)

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

* Translated using Weblate (Korean)

Currently translated at 53.6% (560 of 1044 strings)

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

* Translated using Weblate (Czech)

Currently translated at 100.0% (1044 of 1044 strings)

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

* Translated using Weblate (Korean)

Currently translated at 56.4% (589 of 1044 strings)

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

* ios: import/export localizations

---------

Co-authored-by: Ophiushi <41908476+ishi-sama@users.noreply.github.com>
Co-authored-by: John m <jvanmanen@gmail.com>
Co-authored-by: zenobit <zen@osowoso.xyz>
Co-authored-by: 5olivetree <5olivetree+github.com@mailbox.org>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: mlanp <github@lang.xyz>
Co-authored-by: random r <epsilin@yopmail.com>
Co-authored-by: 橙子 <legiorange@163.com>
Co-authored-by: No name <CertainBot@users.noreply.hosted.weblate.org>
Co-authored-by: sith-on-mars <groguko36@pm.me>
Co-authored-by: Bdd55oo <giggzuv9z.eofjx@aleeas.com>
Co-authored-by: Doocter <Undocked1040@proton.me>
2023-04-01 09:00:15 +01:00
Evgeny Poberezkin 6f304bc9e6 Merge branch 'stable' 2023-04-01 08:12:58 +01:00
Evgeny Poberezkin 1ca0dfffa0 docs: update the process to move profile 2023-04-01 08:12:48 +01:00
Evgeny Poberezkin 1420084f5e website: simplex icon in footer 2023-03-31 22:58:57 +01:00
Evgeny Poberezkin 3e03474437 readme: update translation contributors 2023-03-31 22:19:27 +01:00
Evgeny Poberezkin 95366e4d1b readme: update translations 2023-03-31 19:25:59 +01:00
Evgeny Poberezkin df1775a1e6 website: enable Arabic, Chinese, Spanish 2023-03-31 19:14:39 +01:00
Evgeny Poberezkin 30ccea18ab website: translations (#2113)
* Translated using Weblate (Spanish)

Currently translated at 46.4% (98 of 211 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 46.4% (98 of 211 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 46.4% (98 of 211 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 46.4% (98 of 211 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 46.4% (98 of 211 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 46.4% (98 of 211 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 46.4% (98 of 211 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 46.4% (98 of 211 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 46.4% (98 of 211 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 46.4% (98 of 211 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 46.4% (98 of 211 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 46.4% (98 of 211 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 46.4% (98 of 211 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 46.4% (98 of 211 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 46.4% (98 of 211 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 46.4% (98 of 211 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 46.4% (98 of 211 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 46.4% (98 of 211 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 46.4% (98 of 211 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 46.4% (98 of 211 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 46.4% (98 of 211 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 46.4% (98 of 211 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 46.4% (98 of 211 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 46.4% (98 of 211 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 46.4% (98 of 211 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 46.4% (98 of 211 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 46.4% (98 of 211 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 46.4% (98 of 211 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 46.4% (98 of 211 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 46.4% (98 of 211 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 46.4% (98 of 211 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 46.4% (98 of 211 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 46.4% (98 of 211 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 46.4% (98 of 211 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 46.4% (98 of 211 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 46.4% (98 of 211 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 46.4% (98 of 211 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 46.4% (98 of 211 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 46.4% (98 of 211 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 46.4% (98 of 211 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 46.4% (98 of 211 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 46.4% (98 of 211 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 58.2% (123 of 211 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 100.0% (211 of 211 strings)

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

* Translated using Weblate (German)

Currently translated at 100.0% (211 of 211 strings)

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (211 of 211 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (211 of 211 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 100.0% (211 of 211 strings)

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

* Translated using Weblate (Arabic)

Currently translated at 97.1% (205 of 211 strings)

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

* Translated using Weblate (Arabic)

Currently translated at 99.0% (209 of 211 strings)

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

* fix strong tag in ar.json

---------

Co-authored-by: 4 Bi 5aYzVk 93FCVjWLWxh44XH3984teVSfjwFYmUGUrbvnHwGirk9 <userfifteen.seventeen@mailfence.com>
Co-authored-by: No name <CertainBot@users.noreply.hosted.weblate.org>
Co-authored-by: mlanp <github@lang.xyz>
Co-authored-by: random r <epsilin@yopmail.com>
Co-authored-by: Ophiushi <41908476+ishi-sama@users.noreply.github.com>
Co-authored-by: jonnysemon <johndevand@tutanota.com>
2023-03-31 19:13:19 +01:00
M Sarmad Qadeer 4cd90d74ad website: add RTL languages compatibility (#2056)
* website: add RTL languages compatibility

* website: add few changes

- update tailwindcss version
- add few stylings
- move to rtl true false approach

* website: set lang:en to rtl:true for testing

* website: add arabic key values & textual flag

* website: fix strong tag issues in ar translation.

* website: flip navbar for rtl languages

* disable Arabic

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-03-31 17:14:20 +01:00
spaced4ndy 7f1214688a core: 4.6.1.0 2023-03-31 19:19:51 +04:00
spaced4ndy aa89d0d156 android: video progress, video & image cancelled indicators; ios: image cancelled indicator (#2111) 2023-03-31 19:15:37 +04:00
spaced4ndy 787cd94362 core: support fallback to SMP file transfer for backwards compatibility (#2110) 2023-03-31 17:33:52 +04:00
Evgeny Poberezkin ec61a7fc51 android: reduce video player opacity in the gallery 2023-03-31 13:59:40 +01:00
Evgeny Poberezkin 9b627534f5 android: update video item layout, add video behind experimental toggle (#2109)
* android: update video item layout, add video behind experimental toggle

* video duration / size design

* refactor, fix duration box size

* more readable

* reuse box modifier

* Revert "reuse box modifier"

This reverts commit d0d2d3e402.
2023-03-31 13:40:25 +01:00
Stanislav Dmitrenko 400a3707b2 android: video support (#2102)
* android: video support

* better landscape videos, UI styling

* removed volume control

* quote for video

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-03-31 12:25:13 +01:00
Evgeny Poberezkin 38a5676b37 4.6.1-beta.0: Android 109, iOS 136 2023-03-30 20:12:48 +01:00
spaced4ndy f00cfa9108 core, mobile: CRSndFileCompleteXFTP event (#2107) 2023-03-30 19:45:18 +04:00
Evgeny Poberezkin afa24722b2 Merge branch 'stable' 2023-03-30 15:45:45 +01:00
Evgeny Poberezkin ea5cec53bc update readme (#2106)
* update readme

* corrections

* update roadmap

* link to guide

* remove duplication
2023-03-30 15:44:57 +01:00
Evgeny Poberezkin 61dc649c70 guide: initial readme (#2006)
* guide: initial readme

* update guide

* guide:  initial documentation for audio and video calls (#2104)

* Added documentation for audio and video calls.

* Minor update

* corrections

---------

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

* remove trailing spaces and typos

* docs: update audio-video calls guide

* add app settings

* edit guide, link images and posts

* fix image links

* update

* update 2

* remove link

* remove spaces

* move images

* bold

* add image

---------

Co-authored-by: Silent-Ninja <128339587+silent-ninja-1@users.noreply.github.com>
2023-03-30 15:39:35 +01:00
spaced4ndy b20824e16c core: notify about xftp errors (#2105) 2023-03-30 18:36:39 +04:00
Evgeny Poberezkin 39330fdce3 guide: initial readme (#2006)
* guide: initial readme

* update guide

* guide:  initial documentation for audio and video calls (#2104)

* Added documentation for audio and video calls.

* Minor update

* corrections

---------

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

* remove trailing spaces and typos

* docs: update audio-video calls guide

* add app settings

* edit guide, link images and posts

* fix image links

* update

* update 2

* remove link

* remove spaces

* move images

* bold

* add image

---------

Co-authored-by: Silent-Ninja <128339587+silent-ninja-1@users.noreply.github.com>
2023-03-30 15:17:13 +01:00
spaced4ndy 6b725a8ef7 ios, android: cancel file UI; core: cancel file fixes (#2100)
backend fixes:
- check file is not complete on CancelFile,
- check file is not cancelled when processing XFTP events,
- mark SMP file cancelled if recipient cancelled in direct chat.
2023-03-30 14:10:13 +04:00
Evgeny Poberezkin cbcdeb2b43 ios: 4.6 (135), remove bluetooth-central from background modes (#2086) 2023-03-30 09:07:28 +01:00
Evgeny Poberezkin 4351610eca android: confirm password when deleting/unhiding inactive hidden user profile (#2103) 2023-03-30 09:02:57 +01:00
Evgeny Poberezkin 935d826a21 core, ios: unhiding user profiles always requires password (#2101) 2023-03-29 19:28:06 +01:00
Evgeny Poberezkin a8c8137ade core: fix current user becoming incorrect after hiding or (un)muting inactive user profile (#2098)
* core: fix current user becoming incorrect after hiding or (un)muting inactive user profile

* refactor test
2023-03-29 17:39:04 +01:00
spaced4ndy 7b33e1fba8 core: update cancel file api (#2097) 2023-03-29 17:18:44 +04:00
Evgeny Poberezkin ade7bba97b ios: confirm password when deleting active hidden user (#2095) 2023-03-29 14:01:24 +01:00
spaced4ndy 08dd321311 android: rcv & snd files progress, distinguish XFTP and SMP; ios: files UI improvements (#2096) 2023-03-29 15:48:00 +04:00
Evgeny Poberezkin 67961180c9 android: developer tools page (#2094) 2023-03-29 09:34:55 +01:00
Evgeny Poberezkin 1093892ede ios: update developer options (#2091)
* ios: update developer options

* move XFTP option, make chat console usable

* update footer

* typo

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

---------

Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
2023-03-29 08:41:13 +01:00
spaced4ndy ef05fa4905 core: file protocol field; ios: distinguish behavior and look of XFTP and SMP files (#2090)
* core: file protocol field; ios: distinguish behavior and look of XFTP and SMP files

* remove unused method

* count style

* corrections

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-03-28 19:20:06 +01:00
Evgeny Poberezkin 6a99a4f1ae Merge pull request #2087 from simplex-chat/ep/android-down-migrations
android: support down migrations
2023-03-28 16:53:57 +01:00
Evgeny Poberezkin 4895f396a2 corrections 2023-03-28 16:00:18 +01:00
Evgeny Poberezkin c3dffc5909 fix 2023-03-28 14:14:09 +01:00
spaced4ndy af73e5993d core: test xftp group file transfer (#2088) 2023-03-28 15:22:42 +04:00
spaced4ndy dfec1cbb02 core: add protocol field to files table (#2089) 2023-03-28 14:38:36 +04:00
Evgeny Poberezkin 8d8f7b2524 migrations UI 2023-03-28 11:26:45 +01:00
Evgeny Poberezkin db7b81587f Merge branch 'master' into ep/android-down-migrations 2023-03-27 23:21:41 +01:00
Evgeny Poberezkin 31bb744ba7 android: support down migrations 2023-03-27 23:20:47 +01:00
Evgeny Poberezkin f4b349162f Merge pull request #1998 from simplex-chat/xftp
core: transfer files via XFTP
2023-03-27 23:18:34 +01:00
Evgeny Poberezkin 7f8adf8f03 Merge branch 'master' into xftp 2023-03-27 20:49:47 +01:00
Evgeny Poberezkin 1f4bb8a224 ios: fix picker heights 2023-03-27 20:43:39 +01:00
Evgeny Poberezkin 0c3dc8a6e9 core: add down migrations and fix test 2023-03-27 19:39:22 +01:00
Evgeny Poberezkin 1f15cf54af Merge branch 'master' into xftp 2023-03-27 18:57:14 +01:00
Evgeny Poberezkin c96ba30018 core: support down migrations to allow reverting to the previous version (#2072)
* core: support down migrations to allow reverting to the previous version

* update schema

* update simplexmq

* rename errors

* remove unused functions

* migration UI, test migration

* update migration UI

* return current migrations in CRVersionInfo

* update simplexmq

* test down migrations

* cleanup ios

* show migrations in log
2023-03-27 18:34:48 +01:00
spaced4ndy ffea61917d ios: display rcv & snd files progress (#2085)
* ios: display rcv & snd files progress

* remove animation
2023-03-27 18:02:54 +01:00
Stanislav Dmitrenko f5c11b8faf android: ability to change profile from share dialog, mobile: do not show profile dropdown when there is only one visible profile (#2084)
* android: ability to change profile from share dialog

* icons swap
2023-03-27 17:58:14 +01:00
Stanislav Dmitrenko 48b4b23204 android: make lint happy (#2081) 2023-03-27 15:35:35 +01:00
Evgeny Poberezkin 9df78c8ac8 core: fix video message JSON encoding (#2082) 2023-03-27 15:35:01 +01:00
Stanislav Dmitrenko 450bfe2e17 android: small layout change in moderated item (#2083) 2023-03-27 15:11:17 +01:00
Evgeny Poberezkin c79eb36a7a core: update file status on XFTP progress events (#2079)
* core: update file status on XFTP progress events

* update simplexmq
2023-03-27 12:37:22 +01:00
Evgeny Poberezkin a58b3a42db Merge branch 'stable' 2023-03-27 12:23:42 +01:00
Evgeny Poberezkin e344958224 blog: v4.6 announcement (#2078)
* blog: v4.6 announcement

* update post

* corrections

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

---------

Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
2023-03-27 12:23:23 +01:00
Evgeny Poberezkin 05c4a6c682 android: support for ARMv7a and Android 8+ (#2038)
* add armv7a

* disable armv6l, that is lacking SMP atomics

* Add Android 8 setting (API Ver 26)

* Drop x86_64-linux, this makes no sense with `pkgs' = androidPkgs`.

* Drop mis-labled x86_64-linux:lib:support (it was aarch64-android)

* Drop x86_64-android, these do not exist in nixpkgs

The ones set up were aarch64-android anyway (pkgs' = androidPkgs)

* android: support Android 8+, armeabi-v7a (32 bit) (#2012)

* test

* stubs for allowing to launch the app

* more stubs and minSdk lowered to 26

* replaced functions that supported on higher API levels with other functions

* animated images on lower API levels and write permission

* updated abi filter and scripts for downloading libs

* changed compression script for multiple apks

* cmake changes

---------

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

* update haskell.nix ref

* bump hackage

* bump haskell.nix (again)

* build-android: add armv7

* flake.nix: remove local nixconf

This change to flake.nix breaks build-android.sh script by forcing user
to input y/n. AFAIK, this cannot be automated and I rather not include
workarounds like piping "yes n | nix build ...".

* build-android.sh: update nix version

* flake.{nix,lock}: testing

* flake.{nix,lock}: restore to original

* update android/prepare script to use zip archives

* update gradle file

* android: 4.6-beta.0 (104)

* android: abi filter for bundle (#2075)

* android: abi filter for bundle

* removed log

---------

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

---------

Co-authored-by: Moritz Angermann <moritz.angermann@gmail.com>
Co-authored-by: Avently <7953703+avently@users.noreply.github.com>
Co-authored-by: shum <shum@liber.li>
2023-03-25 21:51:27 +00:00
Evgeny Poberezkin b2aec6d6a7 4.6: Android 107, iOS 134 2023-03-25 17:40:37 +00:00
Evgeny Poberezkin 09c4609b6c website: translations (#2077)
* Translated using Weblate (Dutch)

Currently translated at 100.0% (211 of 211 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 14.2% (30 of 211 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 14.2% (30 of 211 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 100.0% (211 of 211 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 14.6% (31 of 211 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 14.6% (31 of 211 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 15.1% (32 of 211 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 15.1% (32 of 211 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 15.6% (33 of 211 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 15.6% (33 of 211 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 26.5% (56 of 211 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 26.5% (56 of 211 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 28.9% (61 of 211 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 28.9% (61 of 211 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 30.3% (64 of 211 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 30.3% (64 of 211 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 31.2% (66 of 211 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 32.7% (69 of 211 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 32.7% (69 of 211 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 100.0% (211 of 211 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 14.2% (30 of 211 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 14.2% (30 of 211 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 100.0% (211 of 211 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 14.6% (31 of 211 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 14.6% (31 of 211 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 15.1% (32 of 211 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 15.1% (32 of 211 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 15.6% (33 of 211 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 15.6% (33 of 211 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 26.5% (56 of 211 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 26.5% (56 of 211 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 28.9% (61 of 211 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 28.9% (61 of 211 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 30.3% (64 of 211 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 30.3% (64 of 211 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 31.2% (66 of 211 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 32.7% (69 of 211 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 32.7% (69 of 211 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 46.4% (98 of 211 strings)

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

---------

Co-authored-by: John m <jvanmanen@gmail.com>
Co-authored-by: No name <CertainBot@users.noreply.hosted.weblate.org>
Co-authored-by: 4 Bi 5aYzVk 93FCVjWLWxh44XH3984teVSfjwFYmUGUrbvnHwGirk9 <userfifteen.seventeen@mailfence.com>
2023-03-25 17:24:40 +00:00
Evgeny Poberezkin f6d2aa7aae mobile: translations (#2076)
* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (1008 of 1008 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (940 of 940 strings)

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

* Translated using Weblate (Czech)

Currently translated at 99.2% (933 of 940 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (1008 of 1008 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (940 of 940 strings)

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

* Translated using Weblate (Spanish)

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

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

* Translated using Weblate (German)

Currently translated at 100.0% (940 of 940 strings)

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

* Translated using Weblate (Russian)

Currently translated at 100.0% (940 of 940 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (940 of 940 strings)

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

* Translated using Weblate (Italian)

Currently translated at 99.9% (1007 of 1008 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% (940 of 940 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% (940 of 940 strings)

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

* ios: import/export localizations

---------

Co-authored-by: sith-on-mars <groguko36@tuta.io>
Co-authored-by: No name <CertainBot@users.noreply.hosted.weblate.org>
Co-authored-by: John m <jvanmanen@gmail.com>
2023-03-25 17:21:38 +00:00
Evgeny Poberezkin 92facf58f7 android: fix layout of moderated item 2023-03-25 17:02:15 +00:00
Evgeny Poberezkin 15c36c5a84 android: fix JSON parsing for errors not defined in the UI 2023-03-25 16:27:39 +00:00
Evgeny Poberezkin cea0543e98 android: make search field always visible in user profiles view 2023-03-25 16:10:46 +00:00
Evgeny Poberezkin c0bbe77788 android: fix deleting active and hidden user profile, fix incorrect log 2023-03-25 15:25:52 +00:00
Evgeny Poberezkin 9f8cbe140d android: minor lint fixes 2023-03-25 10:25:17 +00:00
Evgeny Poberezkin d0cf550b51 4.6-beta.2: Android 133, iOS 106 2023-03-25 08:28:29 +00:00
Evgeny Poberezkin a86725480f Translated using Weblate (Dutch) (#2073)
Currently translated at 100.0% (211 of 211 strings)

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

Co-authored-by: John m <jvanmanen@gmail.com>
2023-03-24 22:51:50 +00:00
Evgeny Poberezkin 9ad22e1f6d mobile: translations (#2062)
* Translated using Weblate (Russian)

Currently translated at 99.8% (1007 of 1009 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 96.8% (977 of 1009 strings)

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

* Translated using Weblate (Russian)

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

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

* Translated using Weblate (Dutch)

Currently translated at 98.1% (990 of 1009 strings)

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

* Translated using Weblate (Russian)

Currently translated at 99.8% (1007 of 1009 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 96.8% (977 of 1009 strings)

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

* Translated using Weblate (Russian)

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

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

* Translated using Weblate (Dutch)

Currently translated at 98.1% (990 of 1009 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 98.4% (993 of 1009 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 99.2% (1001 of 1009 strings)

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

* Translated using Weblate (French)

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

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 98.6% (995 of 1009 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 96.4% (906 of 939 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 100.0% (1009 of 1009 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% (939 of 939 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 99.1% (1000 of 1009 strings)

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (1009 of 1009 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% (939 of 939 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 96.0% (969 of 1009 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% (1009 of 1009 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% (939 of 939 strings)

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

* Added translation using Weblate (Finnish)

* Added translation using Weblate (Finnish)

* Translated using Weblate (German)

Currently translated at 96.6% (975 of 1009 strings)

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

* Translated using Weblate (Spanish)

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

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

* Translated using Weblate (German)

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

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

* Translated using Weblate (Lithuanian)

Currently translated at 4.7% (48 of 1009 strings)

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

* Translated using Weblate (Lithuanian)

Currently translated at 5.6% (53 of 939 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (1009 of 1009 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (939 of 939 strings)

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

* Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (1009 of 1009 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (1009 of 1009 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (939 of 939 strings)

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

* Translated using Weblate (Dutch)

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

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

* Translated using Weblate (German)

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

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 99.9% (1007 of 1008 strings)

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

* Translated using Weblate (Chinese (Traditional))

Currently translated at 99.9% (1007 of 1008 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 100.0% (1008 of 1008 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 99.9% (1007 of 1008 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 99.3% (1001 of 1008 strings)

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

* ios: import/export localizations

---------

Co-authored-by: John m <jvanmanen@gmail.com>
Co-authored-by: Ophiushi <41908476+ishi-sama@users.noreply.github.com>
Co-authored-by: sith-on-mars <groguko36@tuta.io>
Co-authored-by: zenobit <zen@osowoso.xyz>
Co-authored-by: random r <epsilin@yopmail.com>
Co-authored-by: No name <CertainBot@users.noreply.hosted.weblate.org>
Co-authored-by: mlanp <github@lang.xyz>
Co-authored-by: Moo <hazap@hotmail.com>
Co-authored-by: Float <float.hu+@gmail.com>
2023-03-24 22:50:09 +00:00
Stanislav Dmitrenko a266bcbae7 android: hidden and muted user profiles (#2069)
* android: hidden and muted user profiles

* swap buttons

* smaller delay

* remove unused type

* some fixes of issues

* small visual changes

* removed delay

* re-appeared calls

* update icons and colors

* disable all notifications for muted users

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-03-24 21:48:34 +00:00
spaced4ndy 1ba210fe77 android: support XFTP files (#2070) 2023-03-24 19:06:36 +04:00
spaced4ndy 7898395359 Merge branch 'master' into xftp 2023-03-24 16:27:23 +04:00
spaced4ndy aeb732c2f6 ios: support XFTP files (#2064) 2023-03-24 15:20:15 +04:00
Evgeny Poberezkin b665dce383 ios: show muted user profiles in user menu, do not show badge on messages in hidden profiles (#2068) 2023-03-23 23:09:37 +00:00
Evgeny Poberezkin f349f124d8 4.6-beta.1: Android 105, iOS 132 2023-03-23 22:02:45 +00:00
Evgeny Poberezkin 8212d7a00e mobile: fix "delete for me" moderating the received item in group (#2067) 2023-03-23 18:47:55 +00:00
Stanislav Dmitrenko 36bcb1b26e android: downgrade target sdk (#2065) 2023-03-23 16:04:53 +00:00
spaced4ndy 8d6fe2be99 core: restore stateTVar imports 2023-03-23 17:29:04 +04:00
Evgeny Poberezkin d9571c70f2 update script to unpack ios libs 2023-03-23 10:06:13 +00:00
spaced4ndy babbca48f8 Merge branch 'master' into xftp 2023-03-23 13:58:23 +04:00
Stanislav Dmitrenko 8c4e2e57f9 android: Show lockscreen faster (#1822)
Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-03-23 09:51:23 +00:00
Evgeny Poberezkin 8308651f44 android: 4.6-beta.0 (104) - for f-droid, the build is in armv7a branch 2023-03-22 22:40:35 +00:00
Evgeny Poberezkin ad881bd46a ios: 4.6 (131) 2023-03-22 22:10:31 +00:00
Evgeny Poberezkin c037eb2d24 Revert "mobile: hide observer role from UI (to be reverted after v4.5.4 is released)"
This reverts commit 37d0bc2f14.
2023-03-22 21:38:12 +00:00
Evgeny Poberezkin 8b4353deba ios: 4.6 (130) 2023-03-22 21:35:57 +00:00
Evgeny Poberezkin a2be0d35fb readme: Spanish translation 2023-03-22 20:50:50 +00:00
Evgeny Poberezkin 1909bdc702 android: update string 2023-03-22 20:14:52 +00:00
Evgeny Poberezkin 63909defaf website: translations (#2061)
* Translated using Weblate (Arabic)

Currently translated at 96.2% (203 of 211 strings)

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

* Translated using Weblate (Arabic)

Currently translated at 96.2% (203 of 211 strings)

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

* Translated using Weblate (Arabic)

Currently translated at 97.1% (205 of 211 strings)

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

* Added translation using Weblate (Ukrainian)

* Added translation using Weblate (Chinese (Simplified))

* Translated using Weblate (Dutch)

Currently translated at 100.0% (211 of 211 strings)

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

* Translated using Weblate (Italian)

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

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (211 of 211 strings)

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

* Deleted translation using Weblate (Norwegian Bokmål)

* Translated using Weblate (German)

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

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

* Translated using Weblate (Spanish)

Currently translated at 13.7% (29 of 211 strings)

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

---------

Co-authored-by: jonnysemon <johndevand@tutanota.com>
Co-authored-by: John m <jvanmanen@gmail.com>
Co-authored-by: random r <epsilin@yopmail.com>
Co-authored-by: 4 Bi 5aYzVk 93FCVjWLWxh44XH3984teVSfjwFYmUGUrbvnHwGirk9 <userfifteen.seventeen@mailfence.com>
Co-authored-by: Float <float.hu+@gmail.com>
Co-authored-by: mlanp <github@lang.xyz>
2023-03-22 19:42:54 +00:00
Evgeny Poberezkin 8544984b17 core: 4.6.0.0 2023-03-22 19:37:02 +00:00
Stanislav Dmitrenko 563984c0df android: strings for user privacy settings (#2060) 2023-03-22 19:34:43 +00:00
Evgeny Poberezkin 6500ee5fc9 mobile app translations (#2029)
* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (907 of 907 strings)

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

* Translated using Weblate (Chinese (Traditional))

Currently translated at 1.2% (11 of 907 strings)

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

* Translated using Weblate (Chinese (Traditional))

Currently translated at 1.2% (11 of 907 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 71.7% (692 of 965 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% (965 of 965 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.6% (904 of 907 strings)

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

* Translated using Weblate (Dutch)

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

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (907 of 907 strings)

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

* Translated using Weblate (Chinese (Traditional))

Currently translated at 1.2% (11 of 907 strings)

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

* Translated using Weblate (Chinese (Traditional))

Currently translated at 1.2% (11 of 907 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 71.7% (692 of 965 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% (965 of 965 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.6% (904 of 907 strings)

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

* Translated using Weblate (Dutch)

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

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

* Added translation using Weblate (Ukrainian)

* Added translation using Weblate (Ukrainian)

* Added translation using Weblate (Lithuanian)

* Added translation using Weblate (Lithuanian)

* Translated using Weblate (Spanish)

Currently translated at 84.8% (819 of 965 strings)

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

* Translated using Weblate (Lithuanian)

Currently translated at 0.8% (8 of 965 strings)

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

* Translated using Weblate (Lithuanian)

Currently translated at 0.9% (9 of 907 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (965 of 965 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (907 of 907 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 96.8% (935 of 965 strings)

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (907 of 907 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (965 of 965 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (907 of 907 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 100.0% (965 of 965 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% (965 of 965 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 41.7% (379 of 907 strings)

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

* Translated using Weblate (Czech)

Currently translated at 100.0% (907 of 907 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 100.0% (967 of 967 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 44.9% (408 of 907 strings)

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

* Deleted translation using Weblate (Norwegian Bokmål)

* Deleted translation using Weblate (Polish)

* Deleted translation using Weblate (Norwegian Bokmål)

* Deleted translation using Weblate (Polish)

* Deleted translation using Weblate (Bulgarian)

* Deleted translation using Weblate (Bulgarian)

* Translated using Weblate (German)

Currently translated at 99.2% (900 of 907 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% (967 of 967 strings)

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

* Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (967 of 967 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 100.0% (967 of 967 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 73.2% (664 of 907 strings)

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

* Translated using Weblate (French)

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

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (967 of 967 strings)

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

* Translated using Weblate (Dutch)

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

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

* Translated using Weblate (German)

Currently translated at 100.0% (967 of 967 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 99.4% (902 of 907 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% (967 of 967 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 85.4% (775 of 907 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% (967 of 967 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% (907 of 907 strings)

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

* Translated using Weblate (German)

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

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

* Translated using Weblate (Czech)

Currently translated at 100.0% (967 of 967 strings)

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

* ios: add Spanish

* ios: import localizations

* export localizations

* fix typo

* android: add Spanish

---------

Co-authored-by: sith-on-mars <groguko36@tuta.io>
Co-authored-by: Nick Lai <nick20080808@gmail.com>
Co-authored-by: No name <CertainBot@users.noreply.hosted.weblate.org>
Co-authored-by: John m <jvanmanen@gmail.com>
Co-authored-by: Moo <hazap@hotmail.com>
Co-authored-by: random r <epsilin@yopmail.com>
Co-authored-by: Luis Morillo Najarro <luis_cnnvd@hotmail.com>
Co-authored-by: zenobit <zen@osowoso.xyz>
Co-authored-by: Ophiushi <41908476+ishi-sama@users.noreply.github.com>
Co-authored-by: Pedro Licio <amaralrj@gmail.com>
Co-authored-by: mlanp <github@lang.xyz>
2023-03-22 19:33:24 +00:00
spaced4ndy 2a9c138a23 xftp: set xftp config (#2059) 2023-03-22 22:20:12 +04:00
Evgeny Poberezkin 61d8fa02d4 ios: export localizations 2023-03-22 17:50:41 +00:00
Evgeny Poberezkin 0fac2187f0 mobile: what's new in 4.6 (#2058)
* ios: what's new in 4.6

* android: what's new in 4.6
2023-03-22 17:45:55 +00:00
Evgeny Poberezkin 1db61be860 ios: remove unused type 2023-03-22 15:59:48 +00:00
Evgeny Poberezkin 06a0dbd0f2 core, iOS: hidden and muted user profiles (#2025)
* core, ios: profile privacy design

* migration

* core: user profile privacy

* update nix dependencies

* update simplexmq

* import stateTVar

* update core library

* update UI

* update hide/show user profile

* update API, UI, fix test

* update api, UI, test

* update api call

* fix api

* update UI for hidden profiles

* filter notifications on hidden/muted profiles when inactive, alerts

* updates

* update schema, test, icon
2023-03-22 15:58:01 +00:00
spaced4ndy 47c6daf0cc xftp: set app tmp directory (#2054) 2023-03-22 18:48:38 +04:00
Evgeny Poberezkin bcdf502ce6 core: update simplexmq 2023-03-22 09:10:54 +00:00
Stanislav Dmitrenko f9e2f4931a android: group welcome message (#2042) 2023-03-21 23:00:20 +00:00
Stanislav Dmitrenko 8929d15df0 ios: ability to specify welcome message in a group (#2041)
* ios: ability to specify welcome message in a group

* update state in model
2023-03-21 15:15:48 +00:00
spaced4ndy 60d6a47bdb xftp: delete agent rcv files on completion, error, item delete (#2040) 2023-03-21 15:21:14 +04:00
Stanislav Dmitrenko 2f529535b1 android: turn of screen in call (#2037) 2023-03-20 21:17:13 +00:00
Stanislav Dmitrenko 90c9eae283 android: night mode splash screen (#2036) 2023-03-20 18:04:48 +00:00
Stanislav Dmitrenko added6105b android: relay server footer (#2035) 2023-03-20 17:12:15 +00:00
Stanislav Dmitrenko 2df39b5e24 android: catch exceptions while opening a URL (#2034) 2023-03-20 16:18:41 +00:00
Stanislav Dmitrenko 3477dd9400 android: system and in-app language selector (#2033)
* android: system language selector

* in-app language selector

* refactor

* refactor

* different value for Chinese

* change language order/names

* different translation

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-03-20 15:47:09 +00:00
Evgeny Poberezkin 5282551f3d Merge branch 'stable' 2023-03-19 22:59:36 +00:00
Evgeny Poberezkin dcadaaf29b docs: readme (#2031) 2023-03-19 22:58:17 +00:00
Evgeny Poberezkin e9d6baa6ba docs: translations (#2030)
* docs: translations

* update table

* update heading/links

* update table

* update table

* update heading
2023-03-19 20:28:39 +00:00
zenobit 6ae052a7a1 Added translated docs to czech (#1963)
* Added translated docs to czech

* Dates reverted, Added link to CZ WEBRTC.md
2023-03-19 18:10:32 +00:00
Stanislav Dmitrenko 9f750c2516 android: audio session management in calls (#2026)
* android: audio session management in calls

* audio session compatibility with old APIs

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-03-19 17:20:43 +00:00
Evgeny Poberezkin 1fe46834f2 mobile: add Chinese interface language 2023-03-19 17:17:27 +00:00
Evgeny Poberezkin 3db85c7d37 mobile app translations (#2028)
* Translated using Weblate (Russian)

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

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

* Translated using Weblate (French)

Currently translated at 100.0% (903 of 903 strings)

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (903 of 903 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (903 of 903 strings)

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

* Translated using Weblate (Dutch)

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

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

* Translated using Weblate (Russian)

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

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

* Translated using Weblate (French)

Currently translated at 100.0% (903 of 903 strings)

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (903 of 903 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (903 of 903 strings)

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

* Translated using Weblate (Dutch)

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

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

* Translated using Weblate (Spanish)

Currently translated at 59.1% (571 of 965 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 2.9% (27 of 903 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% (965 of 965 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% (903 of 903 strings)

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

* ios: remove unused translations

* ios: import/export localizations

---------

Co-authored-by: No name <CertainBot@users.noreply.hosted.weblate.org>
Co-authored-by: John m <jvanmanen@gmail.com>
2023-03-19 13:35:51 +00:00
Evgeny Poberezkin 1e4f1b8891 ios: export localizations 2023-03-19 12:53:41 +00:00
Evgeny Poberezkin 0fcd6d40ee Merge pull request #2001 from simplex-chat/callkit
iOS: native calls using WebRTC library and CallKit
2023-03-19 12:25:44 +00:00
Evgeny Poberezkin aaa4ffe789 Merge branch 'master' into callkit 2023-03-19 12:14:34 +00:00
Evgeny Poberezkin 8c4720d0cb ios: link to set app interface language 2023-03-19 12:14:10 +00:00
Evgeny Poberezkin 94b97f6097 cli: /db export command 2023-03-19 11:49:30 +00:00
Evgeny Poberezkin 85800d96c8 ios: import/export localizations 2023-03-18 18:02:34 +00:00
Evgeny Poberezkin 3b4c06111a ios: update core library 2023-03-18 17:54:40 +00:00
Evgeny Poberezkin 548d695a82 mobile app translations (#2027)
* Translated using Weblate (Dutch)

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

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

* Translated using Weblate (Czech)

Currently translated at 100.0% (893 of 893 strings)

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

* Translated using Weblate (Czech)

Currently translated at 100.0% (954 of 954 strings)

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

* Translated using Weblate (Dutch)

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

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

* Translated using Weblate (Chinese (Traditional))

Currently translated at 92.6% (884 of 954 strings)

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

* Translated using Weblate (Portuguese (Brazil))

Currently translated at 16.8% (161 of 954 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 16.8% (161 of 954 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 69.1% (660 of 954 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 72.7% (694 of 954 strings)

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

* Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (954 of 954 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 90.1% (860 of 954 strings)

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

* Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (954 of 954 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (954 of 954 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (893 of 893 strings)

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

* Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (954 of 954 strings)

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

* Translated using Weblate (Portuguese (Brazil))

Currently translated at 26.1% (249 of 954 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (954 of 954 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (893 of 893 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (954 of 954 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (893 of 893 strings)

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

* Translated using Weblate (Chinese (Traditional))

Currently translated at 0.1% (1 of 893 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (959 of 959 strings)

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

* Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (959 of 959 strings)

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

* Translated using Weblate (Portuguese (Brazil))

Currently translated at 27.1% (260 of 959 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (959 of 959 strings)

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

* Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (959 of 959 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 100.0% (959 of 959 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% (893 of 893 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 1.1% (11 of 959 strings)

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

* Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (959 of 959 strings)

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

* Translated using Weblate (Portuguese (Brazil))

Currently translated at 35.8% (344 of 959 strings)

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

* Translated using Weblate (German)

Currently translated at 100.0% (965 of 965 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (965 of 965 strings)

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (965 of 965 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (965 of 965 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% (965 of 965 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% (893 of 893 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 2.5% (25 of 965 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 4.8% (43 of 893 strings)

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

* Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (965 of 965 strings)

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

* Translated using Weblate (Czech)

Currently translated at 100.0% (965 of 965 strings)

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

* Translated using Weblate (Chinese (Traditional))

Currently translated at 0.6% (6 of 893 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (965 of 965 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (893 of 893 strings)

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

* Translated using Weblate (Dutch)

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

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (965 of 965 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (893 of 893 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 32.5% (314 of 965 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 32.5% (314 of 965 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 1.7% (16 of 893 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% (965 of 965 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% (893 of 893 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 45.5% (440 of 965 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% (954 of 954 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% (893 of 893 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 100.0% (954 of 954 strings)

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

* Translated using Weblate (Czech)

Currently translated at 100.0% (893 of 893 strings)

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

* Translated using Weblate (Czech)

Currently translated at 100.0% (954 of 954 strings)

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

* Translated using Weblate (Dutch)

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

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

* Translated using Weblate (Chinese (Traditional))

Currently translated at 92.6% (884 of 954 strings)

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

* Translated using Weblate (Portuguese (Brazil))

Currently translated at 16.8% (161 of 954 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 16.8% (161 of 954 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 69.1% (660 of 954 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 72.7% (694 of 954 strings)

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

* Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (954 of 954 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 90.1% (860 of 954 strings)

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

* Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (954 of 954 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (954 of 954 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (893 of 893 strings)

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

* Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (954 of 954 strings)

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

* Translated using Weblate (Portuguese (Brazil))

Currently translated at 26.1% (249 of 954 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (954 of 954 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (893 of 893 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (954 of 954 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (893 of 893 strings)

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

* Translated using Weblate (Chinese (Traditional))

Currently translated at 0.1% (1 of 893 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (959 of 959 strings)

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

* Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (959 of 959 strings)

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

* Translated using Weblate (Portuguese (Brazil))

Currently translated at 27.1% (260 of 959 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (959 of 959 strings)

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

* Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (959 of 959 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 100.0% (959 of 959 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% (893 of 893 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 1.1% (11 of 959 strings)

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

* Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (959 of 959 strings)

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

* Translated using Weblate (Portuguese (Brazil))

Currently translated at 35.8% (344 of 959 strings)

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

* Translated using Weblate (German)

Currently translated at 100.0% (965 of 965 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (965 of 965 strings)

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (965 of 965 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (965 of 965 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% (965 of 965 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% (893 of 893 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 2.5% (25 of 965 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 4.8% (43 of 893 strings)

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

* Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (965 of 965 strings)

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

* Translated using Weblate (Czech)

Currently translated at 100.0% (965 of 965 strings)

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

* Translated using Weblate (Chinese (Traditional))

Currently translated at 0.6% (6 of 893 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (965 of 965 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (893 of 893 strings)

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

* Translated using Weblate (Dutch)

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

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (965 of 965 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (893 of 893 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 32.5% (314 of 965 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 32.5% (314 of 965 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 1.7% (16 of 893 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% (965 of 965 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% (893 of 893 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 45.5% (440 of 965 strings)

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

---------

Co-authored-by: John m <jvanmanen@gmail.com>
Co-authored-by: zenobit <zen@osowoso.xyz>
Co-authored-by: Bdd55oo <giggzuv9z.eofjx@aleeas.com>
Co-authored-by: Pedro Licio <amaralrj@gmail.com>
Co-authored-by: Pedro Licio <pedro@agenciaregex.com>
Co-authored-by: Float <float.hu+@gmail.com>
Co-authored-by: sith-on-mars <groguko36@tuta.io>
Co-authored-by: jonnysemon <johndevand@tutanota.com>
Co-authored-by: mlanp <github@lang.xyz>
Co-authored-by: Ophiushi <41908476+ishi-sama@users.noreply.github.com>
Co-authored-by: random r <epsilin@yopmail.com>
Co-authored-by: Nick Lai <nick20080808@gmail.com>
Co-authored-by: M Sarmad Qadeer <msarmadqadeer@gmail.com>
Co-authored-by: No name <CertainBot@users.noreply.hosted.weblate.org>
Co-authored-by: Luis Morillo Najarro <luis_cnnvd@hotmail.com>
2023-03-18 17:46:50 +00:00
Evgeny Poberezkin c986a4b88b website: enable Italian and Dutch translations 2023-03-18 17:20:08 +00:00
Evgeny Poberezkin 09940ccf8d website translations (#1960)
* Added translation using Weblate (Arabic)

* Translated using Weblate (French)

Currently translated at 100.0% (211 of 211 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (211 of 211 strings)

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

* Translated using Weblate (Arabic)

Currently translated at 20.3% (43 of 211 strings)

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

* Translated using Weblate (Arabic)

Currently translated at 52.6% (111 of 211 strings)

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

* Translated using Weblate (Arabic)

Currently translated at 61.6% (130 of 211 strings)

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

* Added translation using Weblate (Arabic)

* Translated using Weblate (French)

Currently translated at 100.0% (211 of 211 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (211 of 211 strings)

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

* Translated using Weblate (Arabic)

Currently translated at 20.3% (43 of 211 strings)

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

* Translated using Weblate (Arabic)

Currently translated at 52.6% (111 of 211 strings)

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

* Translated using Weblate (Arabic)

Currently translated at 61.6% (130 of 211 strings)

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

* Translated using Weblate (Arabic)

Currently translated at 69.1% (146 of 211 strings)

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

* Translated using Weblate (Czech)

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

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

* Translated using Weblate (Dutch)

Currently translated at 46.4% (98 of 211 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 50.2% (106 of 211 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 95.7% (202 of 211 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 96.6% (204 of 211 strings)

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

* Translated using Weblate (Dutch)

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

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

* Translated using Weblate (German)

Currently translated at 100.0% (211 of 211 strings)

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

* Translated using Weblate (Arabic)

Currently translated at 100.0% (211 of 211 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 100.0% (211 of 211 strings)

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

* Translated using Weblate (German)

Currently translated at 100.0% (211 of 211 strings)

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

* Added translation using Weblate (Italian)

* Translated using Weblate (Italian)

Currently translated at 100.0% (211 of 211 strings)

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

* Added translation using Weblate (Spanish)

* remove "coming soon"

---------

Co-authored-by: Ophiushi <41908476+ishi-sama@users.noreply.github.com>
Co-authored-by: jonnysemon <johndevand@tutanota.com>
Co-authored-by: ManeraKai <manerakai@protonmail.com>
Co-authored-by: zenobit <zen@osowoso.xyz>
Co-authored-by: John m <jvanmanen@gmail.com>
Co-authored-by: mlanp <github@lang.xyz>
Co-authored-by: random r <epsilin@yopmail.com>
2023-03-18 17:12:25 +00:00
Evgeny Poberezkin cfc323862f update simplexmq 2023-03-18 16:28:07 +00:00
Evgeny Poberezkin d8cc867099 update simplexmq 2023-03-18 16:18:59 +00:00
Evgeny Poberezkin dce8a1dff9 update simplexmq 2023-03-18 13:43:01 +00:00
Evgeny Poberezkin 5bc9e014c2 update simplexmq 2023-03-18 13:26:54 +00:00
Evgeny Poberezkin b0c9ba05f3 Merge branch 'master' into xftp 2023-03-18 11:00:30 +00:00
Evgeny Poberezkin 8a2876fca9 core: uncomment simplexmq QQ git reference, ios: update core 2023-03-18 11:00:19 +00:00
Evgeny Poberezkin 00d5f3b769 Merge branch 'master' into xftp 2023-03-18 09:59:25 +00:00
Evgeny Poberezkin 17f39ec6a0 Merge branch 'stable' 2023-03-18 09:59:08 +00:00
Evgeny Poberezkin 498ffe8a71 4.5.4: Android 103, iOS 128 2023-03-18 09:57:19 +00:00
Evgeny Poberezkin 858f0f2650 Merge branch 'master' into xftp 2023-03-18 08:38:27 +00:00
Evgeny Poberezkin 66ea2d5d71 Merge branch 'stable' 2023-03-18 08:38:10 +00:00
Evgeny Poberezkin 3dd5b5d835 core: 4.5.4.2 (add stateTVar imports) 2023-03-18 07:59:43 +00:00
Evgeny Poberezkin 9127b1bbc6 Merge pull request #2022 from simplex-chat/ep/v454
v4.5.4: add support for observer role
2023-03-17 17:14:43 +00:00
Evgeny Poberezkin 1657bcf97d core: 4.5.4.1 2023-03-17 15:02:41 +00:00
Evgeny Poberezkin 428db2f8f4 core: fix unused contact deletion (#2023)
* core: failing test for leaving and deleting the group joined via link

* fix test

* merge logic

* fix

* add condition

* refactor

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

* compiles

---------

Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
2023-03-17 16:03:19 +04:00
Evgeny Poberezkin a8fa9b5e58 Merge branch 'master' into xftp 2023-03-17 09:58:36 +00:00
Evgeny Poberezkin 4cc59d9fbd core: 4.5.4.0 2023-03-16 23:30:28 +00:00
Evgeny Poberezkin c50306709b mobile: do not show "observer" overlay when user leaves group 2023-03-16 23:29:47 +00:00
Evgeny Poberezkin 37d0bc2f14 mobile: hide observer role from UI (to be reverted after v4.5.4 is released) 2023-03-16 22:58:00 +00:00
Stanislav Dmitrenko 2fda0454e3 android: group link role, add observer role (#1981)
* android: group link role, add observer role

* padding

* disabled tint for buttons

* proper layout for long display name

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-03-16 22:20:42 +00:00
Evgeny Poberezkin be19af62d9 ios: group link role, add observer role (#1978)
* ios: group link role, add observer role

* prevent observers from sending in UI, clear compose state on role change
2023-03-16 22:20:20 +00:00
Evgeny Poberezkin f915eb2a20 core: initial group member role when joining via link (#1975)
* core: initial group member role when joining via link

* fix tests

* set role when joining group via link, enable observer test

* show group link when role changes

* amend test

* check role is member or observer when creating a link
2023-03-16 22:19:51 +00:00
Evgeny Poberezkin 2bc1236a2c terminal: update help, remove user ID from terminal /smp test command (#1973)
* terminal: update help, remove user ID from terminal /smp test command

* update mobile api

* update help
2023-03-16 22:19:22 +00:00
Evgeny Poberezkin 9db1924268 ios: optionally show callkit calls in recents and update settings (#2021)
* ios: optionally show callkit calls in recents and update settings

* refactor, fix call error when starting from recents
2023-03-16 22:08:58 +00:00
Evgeny Poberezkin 7a9f220290 ios: do not suspend chat when switching to another callkit call (#2020) 2023-03-16 20:19:53 +00:00
Evgeny Poberezkin 8145387f77 ios: CallKit changed reporting logic (#2019)
* ios: CallKit changed reporting logic

* refactor, suspend chat after call when app is in background

---------

Co-authored-by: Avently <7953703+avently@users.noreply.github.com>
2023-03-16 19:57:43 +00:00
Evgeny Poberezkin 063440e735 ios: remove sheets in ActiveCallView (does not work when call accepted from background via callkit) 2023-03-16 17:18:25 +00:00
Evgeny Poberezkin 6724de09c9 ios: dismiss sheets on IncomingCallView, send notification if reportNewIncomingVoIPPushPayload fails 2023-03-16 16:59:05 +00:00
Evgeny Poberezkin f379fd0f8c xftp: sending file completion status (#2016)
* xftp: sending file completion status

* fix type

* fix type 2

* fix
2023-03-16 13:58:01 +00:00
spaced4ndy 34a3387830 core: xftp servers option; use local xftp server in tests (#2015) 2023-03-16 14:12:19 +04:00
Evgeny Poberezkin 809cc1f234 ios: different speaker buttons on call screen 2023-03-16 08:46:13 +00:00
spaced4ndy 12200a74ff core: XFTP file transfer test (#2009) 2023-03-16 10:49:57 +04:00
Stanislav Dmitrenko 2643ea9066 ios: reverted some changes related to lockScreen (#2011)
* Revert "ios: CallKit enhancements (#2010)"

This reverts commit 840df89ca6.

* Revert "ios: CallKit integrated with app lock and screen protect (#2007)"

This reverts commit 0404b020e6.

* ios: reverted some changes related to lockScreen

* undo delay

* better support of appLock + call

* refactor

* refactor 2

* refactor 3

* refactor 4

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-03-15 21:09:33 +00:00
Stanislav Dmitrenko 840df89ca6 ios: CallKit enhancements (#2010)
* ios: CallKit enhancements

* better checks
2023-03-15 15:32:27 +00:00
Stanislav Dmitrenko 0404b020e6 ios: CallKit integrated with app lock and screen protect (#2007)
* ios: CallKit integrated with app lock and screen protect

* better lock mechanics

* background color

* logs

* refactor, revert auth changes

* additional state variable to allow connecting call

* fix lock screen, public logs

* show callkit option without dev tools

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-03-15 10:21:21 +00:00
spaced4ndy fda41817e9 core: XFTP accept; provide save path to agent (#2005) 2023-03-14 21:51:35 +04:00
Stanislav Dmitrenko f48cabcc0a ios: CallKit double call in background fix (#2004) 2023-03-14 15:28:34 +00:00
Stanislav Dmitrenko f123a905d5 app icon in CallKit screen (#2003) 2023-03-14 15:19:54 +00:00
spaced4ndy 9b7fbfd513 core: rcv file events (#2002) 2023-03-14 15:26:40 +04:00
Evgeny Poberezkin e21b4d4236 xftp: send file descriptions when ready (#1999)
* xftp: send file descriptions when ready

* remove comments, update progress on completion

* update simplexmq

* fix error condition

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

* fix conflict

* saveMemberFD

* more efficient list merging

---------

Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
2023-03-14 13:28:54 +04:00
Stanislav Dmitrenko 9ec6911005 ios: CallKit integration (#1969)
* ios: CallKit integration

* notifying CallKit about outgoing call

* changes

* switching calls with CallKit

* string

* add NSE filtering entitlement

* add NSE build scheme

* remove some call limitations

* calls enhancments

* fixed calls on lockscreen

* don't display useless notification

* fix app state

* ability to answer on call from chat item via CallKit

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-03-14 08:12:40 +00:00
spaced4ndy bfc178faf3 core: process rcv file description (#1997)
* core: process rcv file description

* refactor, groups

* view

* refactor

* update simplexmq

* refactor

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-03-14 11:42:44 +04:00
darkmaster c4c93f881d docs: fixes listening port (server) [FR] (#2000) 2023-03-13 23:57:37 +00:00
Evgeny Poberezkin d7f9e17bcb core: use XFTP to send and receive files (#1993)
* core: use XFTP to send and receive files

* xftp files progress

* xftp reception stubs, migration

* update simplexmq

* xftp sequence diagram

* additional chat events

* send file via XFTP

* send XFTP file description inline when file is uploaded
2023-03-13 10:30:32 +00:00
Evgeny Poberezkin 13706c4f64 website: remove "coming soon" from released features 2023-03-12 23:44:16 +00:00
Evgeny Poberezkin f2f4b26c35 core: update agent protocol to parameterize by entity type (#1988)
* core: update agent protocol to parameterize by entity type

* update simplexmq
2023-03-10 17:23:04 +00:00
Evgeny Poberezkin 1b7b9da07c ios: update core library 2023-03-09 23:08:53 +00:00
Evgeny Poberezkin 2817306659 core: types to support xftp (#1971)
* core: types to support xftp

* migration, amend types

* update protocol / types

* update protocol, types

* update schema, simplexmq
2023-03-09 11:01:22 +00:00
Stanislav Dmitrenko 5f587c2104 android: group link role, add observer role (#1981)
* android: group link role, add observer role

* padding

* disabled tint for buttons

* proper layout for long display name

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-03-07 22:27:28 +00:00
M Sarmad Qadeer f5670c39da website: add support for overlay hash in URL (#1974)
* website: add support for overlay hash in URL

* website: update the overlay hashes

* website: fix the ui of donate button in join simplex section

* website: make the text selectable of unique & explained swiper

* scroll to popup context

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-03-06 22:25:54 +00:00
Stanislav Dmitrenko c0105d135c android: UI to moderate messages to other members (#1982)
* android: UI to moderate messages to other members

* do not show moderate button on moderated, show alert

* changed item

* limiting number of lines in header

* limit text height
2023-03-06 21:58:44 +00:00
Evgeny Poberezkin f1a9814faa ios: UI to moderate messages of other members (#1980)
* ios: UI to moderate messages of other members

* split moderate action

* do not show moderate button on moderated, show alert
2023-03-06 21:57:58 +00:00
Evgeny Poberezkin 8f0e7512be ios: group link role, add observer role (#1978)
* ios: group link role, add observer role

* prevent observers from sending in UI, clear compose state on role change
2023-03-06 13:54:43 +00:00
Evgeny Poberezkin 7d49209f79 core: initial group member role when joining via link (#1975)
* core: initial group member role when joining via link

* fix tests

* set role when joining group via link, enable observer test

* show group link when role changes

* amend test

* check role is member or observer when creating a link
2023-03-06 09:51:42 +00:00
Evgeny Poberezkin b2e285c2c7 terminal: update help, remove user ID from terminal /smp test command (#1973)
* terminal: update help, remove user ID from terminal /smp test command

* update mobile api

* update help
2023-03-04 22:33:17 +00:00
Stanislav Dmitrenko 54020250dc ios: native WebRTC (#1933)
* ios: native WebRTC

* add video showing

* make async function better working with main thread

* wrapped code in main actor, just in case

* small change

* a little better

* enable relay

* removed unused code

* allow switching calls

* testing

* enable encryption

* testing more

* another test

* one more test

* fix remote unencrypted video

* deleted unused code related to PixelBuffer

* added MediaEncryption playground

* better playground

* better playground

* fixes

* use new encryption api

* media encryption works

* small changes

* added lib dependency

* use commit reference for lib instead of version

* video format, PIP size

* remove sample.js

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-03-02 13:17:01 +00:00
Evgeny Poberezkin 01acbb970a blog: typos 2023-03-01 20:03:06 +00:00
Evgeny Poberezkin 36cad35d46 blog: SimpleX File Transfer Protocol (XFTP) (#1965)
* blog: SimpleX File Transfer Protocol (XFTP)

* update blog

* simplify quick start

* update blog
2023-03-01 19:29:59 +00:00
spacedandy 41c9c84139 4.5.3: iOS 127 2023-03-01 19:38:50 +04:00
spacedandy 9e9ca521b0 4.5.3: Android 102 2023-03-01 18:00:49 +04:00
spaced4ndy 1927862871 android: show "export prohibited" alert on enabling app data backup with random db password set (#1962) 2023-03-01 14:36:08 +04:00
spacedandy c80eaf8550 core: 4.5.3.1 2023-03-01 13:23:34 +04:00
Evgeny Poberezkin 9e6a35bac3 mobile: add Czech, fix translations (#1961)
* ios: import localizations

* re-export localizations

* add Czech language

* fix Czech strings

* add Czech to android
2023-03-01 08:52:56 +00:00
Evgeny Poberezkin 62ffcf94a6 translations (#1956)
* Translated using Weblate (Chinese (Simplified))

Currently translated at 73.6% (658 of 893 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 100.0% (954 of 954 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.7% (891 of 893 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 100.0% (893 of 893 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 78.2% (699 of 893 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (893 of 893 strings)

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (893 of 893 strings)

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

* Translated using Weblate (German)

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

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

* Translated using Weblate (Dutch)

Currently translated at 100.0% (893 of 893 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 96.5% (862 of 893 strings)

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

* Added translation using Weblate (Arabic)

* Added translation using Weblate (Arabic)

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (893 of 893 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 100.0% (893 of 893 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 73.6% (658 of 893 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 100.0% (954 of 954 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.7% (891 of 893 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 100.0% (893 of 893 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 78.2% (699 of 893 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (893 of 893 strings)

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (893 of 893 strings)

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

* Translated using Weblate (German)

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

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

* Translated using Weblate (Dutch)

Currently translated at 100.0% (893 of 893 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 96.5% (862 of 893 strings)

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

* Added translation using Weblate (Arabic)

* Added translation using Weblate (Arabic)

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (893 of 893 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 100.0% (893 of 893 strings)

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

* Translated using Weblate (Dutch)

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

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

* Translated using Weblate (Dutch)

Currently translated at 100.0% (893 of 893 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 100.0% (893 of 893 strings)

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

* Translated using Weblate (Dutch)

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

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

* Translated using Weblate (Dutch)

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

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

* Translated using Weblate (Czech)

Currently translated at 100.0% (954 of 954 strings)

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

* Translated using Weblate (Chinese (Traditional))

Currently translated at 59.2% (565 of 954 strings)

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

* Translated using Weblate (Czech)

Currently translated at 100.0% (954 of 954 strings)

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

* Translated using Weblate (Czech)

Currently translated at 100.0% (893 of 893 strings)

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

* Translated using Weblate (Czech)

Currently translated at 100.0% (954 of 954 strings)

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

* Translated using Weblate (Czech)

Currently translated at 100.0% (954 of 954 strings)

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

* Translated using Weblate (Portuguese (Brazil))

Currently translated at 0.6% (6 of 954 strings)

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

---------

Co-authored-by: Aaron H <niximi333@gmail.com>
Co-authored-by: John m <jvanmanen@gmail.com>
Co-authored-by: zenobit <zen@osowoso.xyz>
Co-authored-by: Ophiushi <41908476+ishi-sama@users.noreply.github.com>
Co-authored-by: random r <epsilin@yopmail.com>
Co-authored-by: mlanp <github@lang.xyz>
Co-authored-by: M Sarmad Qadeer <msarmadqadeer@gmail.com>
Co-authored-by: Bdd55oo <giggzuv9z.eofjx@aleeas.com>
Co-authored-by: Pedro Licio <pedro@agenciaregex.com>
2023-03-01 08:15:32 +00:00
Evgeny Poberezkin 2b77920dcd teminal: option to log errors and service messages to file, closes #1516 (#1957)
* teminal: option to log errors and service messages to file, closes #1516

* rename function
2023-02-28 23:26:08 +00:00
darkmaster 38b7e4d4a4 docs: fixes listening port (server) (#1958) 2023-02-27 20:15:08 +00:00
Evgeny Poberezkin 3e4d4f04ef ios: allow pasting profile image 2023-02-27 17:46:10 +00:00
Evgeny Poberezkin f6f3d17383 ios: do not show notifications on update events in inactive profiles (#1959) 2023-02-27 16:20:54 +00:00
Evgeny Poberezkin d5f6b76ec5 core: increase default queue sizes to 1024, fix broadcast bot not to lock when queue is smaller than the number of contacts (#1955) 2023-02-26 16:36:11 +00:00
zenobit ae75be56ea blog: string fix (#1952) 2023-02-25 17:53:31 +00:00
Evgeny Poberezkin 50b90c4814 core: use 12 bytes IV for WebRTC frame encryption with AES-GCM (#1951)
* core: use 12 bytes IV for WebRTC frame encryption with AES-GCM

* refactor
2023-02-25 17:52:23 +00:00
+shyfire131 6eddb5f30f fix comment syntax in cabal.project.mac (#1948)
Co-authored-by: +shyfire131 <shyfire131@shyfire131.net>
2023-02-24 21:04:46 +00:00
Evgeny Poberezkin cee8f3a4b6 website internationalization (#1922)
* website Internationalization (#1904)

* added devcontainer config

* internationalization under dev

* internationalization of _data done

* overlays internationalization done

* improved routing

* updated gitignore

* remove .devcontainer

* internationalization in progess
deleted all the intermediate files
added translation of few sections

* remaining website strings are added to translation

* done internationalization
- fully converted to i18n plugin
- wrote bash script for creating a combined translations.json

* internationalization UI done

* a quick fix

* remove jq installation

* Added translation using Weblate (German)

* Translated using Weblate (French)

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

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

* fixes of web Internationalization (#1925)

* a quick fix

* blog route for all languages is shifted to root blog route

* wrote merge_translations.js

* remove language label from dropdown

* update language names

* refactor scripts

* remove catch from script

* Added translation using Weblate (Dutch)

* Added translation using Weblate (Norwegian Bokmål)

* Translated using Weblate (Dutch)

Currently translated at 33.0% (70 of 212 strings)

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

* website: internationalization fixes (#1931)

* added devcontainer config

* internationalization under dev

* internationalization of _data done

* overlays internationalization done

* improved routing

* updated gitignore

* remove .devcontainer

* internationalization in progess
deleted all the intermediate files
added translation of few sections

* remaining website strings are added to translation

* done internationalization
- fully converted to i18n plugin
- wrote bash script for creating a combined translations.json

* internationalization UI done

* a quick fix

* blog route for all languages is shifted to root blog route

* wrote merge_translations.js

* remove flag from blog

* updated nav stylings & logo links

* add enabled key

* updated nav dropdown styling

* gave specific lang to i18n plugin.
overlay translations are now working.

* enable nl & nb_NO

* updated nav stylings

* updated contact.js

---------

Co-authored-by: M Sarmad Qadeer <msarmadqadeer@gmail.com>

* Translated using Weblate (German)

Currently translated at 47.6% (101 of 212 strings)

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

* fixed internationalization issues

* updated strings, refactor contact.js

* updated nav stylings for mobile

* a bit smaller padding on mobile

* Added translation using Weblate (Czech)

* Translated using Weblate (Czech)

Currently translated at 100.0% (212 of 212 strings)

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

* Translated using Weblate (French)

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

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

* enabled languages

* check/correct (#1949)

---------

Co-authored-by: M Sarmad Qadeer <MSarmadQadeer@gmail.com>
Co-authored-by: Ophiushi <Ophiushi@users.noreply.hosted.weblate.org>
Co-authored-by: mlanp <github@lang.xyz>
Co-authored-by: John m <jvanmanen@gmail.com>
Co-authored-by: zenobit <zen@osowoso.xyz>
Co-authored-by: Ophiushi <41908476+ishi-sama@users.noreply.github.com>
2023-02-24 21:03:57 +00:00
Evgeny Poberezkin a2e5733be6 core: update/fix webrtc frame encryption function to return error (#1950)
* core: update/fix webrtc frame encryption function to return error

* ios: update C header

* more tests
2023-02-24 20:55:59 +00:00
Evgeny Poberezkin 5075657c02 ios: update core library 2023-02-23 23:36:39 +00:00
Evgeny Poberezkin 0450b1ace2 mobile: welcome/about page (#1946)
* mobile: welcome/about page

* scroll view (fixes trimmed texts)

* layout

* bigger frame

* minHeight, to allow scroling with large font

---------

Co-authored-by: spacedandy <8711996+spaced4ndy@users.noreply.github.com>
2023-02-21 15:31:41 +00:00
Evgeny Poberezkin 0ebf1da05d core: WebRTC frames encryption (#1942)
* core: WebRTC frames encryption

* test
2023-02-19 23:51:50 +00:00
Evgeny Poberezkin 07ad3edbc2 4.5.3-beta.0: Android 101, iOS 126 2023-02-19 13:42:32 +00:00
Evgeny Poberezkin b40ed2a7f3 core: 4.5.3.0 2023-02-19 10:09:22 +00:00
Evgeny Poberezkin 29b074607c mobile: add Dutch interface (#1941)
* iOS: add Dutch language

* ios: re-export localizations

* enable Dutch in Android
2023-02-19 08:47:26 +00:00
Evgeny Poberezkin 258a157e44 translations (#1938)
* Translated using Weblate (Italian)

Currently translated at 100.0% (953 of 953 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 46.5% (444 of 953 strings)

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

* Translated using Weblate (Chinese (Simplified))

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

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 50.8% (485 of 953 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% (953 of 953 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 0.3% (3 of 891 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 57.0% (544 of 953 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 51.6% (460 of 891 strings)

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

* Translated using Weblate (Chinese (Traditional))

Currently translated at 15.1% (144 of 953 strings)

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

* Translated using Weblate (Chinese (Traditional))

Currently translated at 15.2% (145 of 953 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 14.3% (128 of 891 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 15.1% (135 of 891 strings)

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

* Translated using Weblate (Hindi)

Currently translated at 14.3% (137 of 953 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 100.0% (953 of 953 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 25.1% (224 of 891 strings)

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

* Translated using Weblate (Hindi)

Currently translated at 15.1% (144 of 953 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 59.4% (567 of 953 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 53.5% (477 of 891 strings)

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

* Translated using Weblate (Hindi)

Currently translated at 15.5% (148 of 953 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 29.2% (261 of 891 strings)

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

* Translated using Weblate (Hindi)

Currently translated at 15.6% (149 of 953 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 31.6% (282 of 891 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 100.0% (953 of 953 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 36.2% (323 of 891 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 100.0% (953 of 953 strings)

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

* Translated using Weblate (Czech)

Currently translated at 100.0% (953 of 953 strings)

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

* Translated using Weblate (Czech)

Currently translated at 100.0% (953 of 953 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 7.4% (71 of 953 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 41.5% (370 of 891 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 100.0% (953 of 953 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 53.9% (481 of 891 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 57.5% (513 of 891 strings)

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

* Added translation using Weblate (Bulgarian)

* Added translation using Weblate (Bulgarian)

* Translated using Weblate (Dutch)

Currently translated at 100.0% (953 of 953 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 77.2% (688 of 891 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 83.9% (748 of 891 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 100.0% (953 of 953 strings)

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

* Translated using Weblate (Chinese (Traditional))

Currently translated at 15.9% (152 of 953 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 100.0% (953 of 953 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% (891 of 891 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 100.0% (953 of 953 strings)

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (954 of 954 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 7.9% (76 of 954 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 99.8% (953 of 954 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% (891 of 891 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 99.8% (953 of 954 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% (891 of 891 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 99.8% (953 of 954 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% (891 of 891 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 100.0% (954 of 954 strings)

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

* Added translation using Weblate (Norwegian Bokmål)

* Added translation using Weblate (Norwegian Bokmål)

* Translated using Weblate (German)

Currently translated at 100.0% (954 of 954 strings)

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

* Translated using Weblate (French)

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

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

* Translated using Weblate (Japanese)

Currently translated at 67.1% (641 of 954 strings)

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

* Translated using Weblate (Hindi)

Currently translated at 15.7% (150 of 954 strings)

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

* Translated using Weblate (Japanese)

Currently translated at 99.3% (948 of 954 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 61.4% (586 of 954 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 55.5% (495 of 891 strings)

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

* Translated using Weblate (Chinese (Traditional))

Currently translated at 27.2% (260 of 954 strings)

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

* Translated using Weblate (Chinese (Traditional))

Currently translated at 42.5% (406 of 954 strings)

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

* Translated using Weblate (Croatian)

Currently translated at 2.0% (18 of 891 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 66.2% (632 of 954 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 61.3% (547 of 891 strings)

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

* Translated using Weblate (Russian)

Currently translated at 100.0% (954 of 954 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 100.0% (891 of 891 strings)

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (953 of 953 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 46.5% (444 of 953 strings)

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

* Translated using Weblate (Chinese (Simplified))

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

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 50.8% (485 of 953 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% (953 of 953 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 0.3% (3 of 891 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 57.0% (544 of 953 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 51.6% (460 of 891 strings)

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

* Translated using Weblate (Chinese (Traditional))

Currently translated at 15.1% (144 of 953 strings)

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

* Translated using Weblate (Chinese (Traditional))

Currently translated at 15.2% (145 of 953 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 14.3% (128 of 891 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 15.1% (135 of 891 strings)

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

* Translated using Weblate (Hindi)

Currently translated at 14.3% (137 of 953 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 100.0% (953 of 953 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 25.1% (224 of 891 strings)

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

* Translated using Weblate (Hindi)

Currently translated at 15.1% (144 of 953 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 59.4% (567 of 953 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 53.5% (477 of 891 strings)

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

* Translated using Weblate (Hindi)

Currently translated at 15.5% (148 of 953 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 29.2% (261 of 891 strings)

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

* Translated using Weblate (Hindi)

Currently translated at 15.6% (149 of 953 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 31.6% (282 of 891 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 100.0% (953 of 953 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 36.2% (323 of 891 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 100.0% (953 of 953 strings)

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

* Translated using Weblate (Czech)

Currently translated at 100.0% (953 of 953 strings)

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

* Translated using Weblate (Czech)

Currently translated at 100.0% (953 of 953 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 7.4% (71 of 953 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 41.5% (370 of 891 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 100.0% (953 of 953 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 53.9% (481 of 891 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 57.5% (513 of 891 strings)

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

* Added translation using Weblate (Bulgarian)

* Added translation using Weblate (Bulgarian)

* Translated using Weblate (Dutch)

Currently translated at 100.0% (953 of 953 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 77.2% (688 of 891 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 83.9% (748 of 891 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 100.0% (953 of 953 strings)

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

* Translated using Weblate (Chinese (Traditional))

Currently translated at 15.9% (152 of 953 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 100.0% (953 of 953 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% (891 of 891 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 100.0% (953 of 953 strings)

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (954 of 954 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 7.9% (76 of 954 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 99.8% (953 of 954 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% (891 of 891 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 99.8% (953 of 954 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% (891 of 891 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 99.8% (953 of 954 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% (891 of 891 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 100.0% (954 of 954 strings)

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

* Added translation using Weblate (Norwegian Bokmål)

* Added translation using Weblate (Norwegian Bokmål)

* Translated using Weblate (German)

Currently translated at 100.0% (954 of 954 strings)

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

* Translated using Weblate (French)

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

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

* Translated using Weblate (Japanese)

Currently translated at 67.1% (641 of 954 strings)

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

* Translated using Weblate (Hindi)

Currently translated at 15.7% (150 of 954 strings)

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

* Translated using Weblate (Japanese)

Currently translated at 99.3% (948 of 954 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 61.4% (586 of 954 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 55.5% (495 of 891 strings)

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

* Translated using Weblate (Chinese (Traditional))

Currently translated at 27.2% (260 of 954 strings)

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

* Translated using Weblate (Chinese (Traditional))

Currently translated at 42.5% (406 of 954 strings)

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

* Translated using Weblate (Croatian)

Currently translated at 2.0% (18 of 891 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 66.2% (632 of 954 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 61.3% (547 of 891 strings)

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

* Translated using Weblate (Russian)

Currently translated at 100.0% (954 of 954 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 100.0% (891 of 891 strings)

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

---------

Co-authored-by: random r <epsilin@yopmail.com>
Co-authored-by: xqhjay <137847720@qq.com>
Co-authored-by: sith-on-mars <groguko36@pm.me>
Co-authored-by: John m <jvanmanen@gmail.com>
Co-authored-by: zenobit <zen@osowoso.xyz>
Co-authored-by: sith-on-mars <groguko36@tuta.io>
Co-authored-by: Bdd55oo <giggzuv9z.eofjx@aleeas.com>
Co-authored-by: Raman <translations.0l5zc@simplelogin.com>
Co-authored-by: LegendaryInfernalFox <0387agimeno@e-itaca.es>
Co-authored-by: mlanp <github@lang.xyz>
Co-authored-by: Ophiushi <Ophiushi@users.noreply.hosted.weblate.org>
Co-authored-by: tomato potato <4ryo49@protonmail.com>
Co-authored-by: Mehmed <pajazetovicmeho@gmail.com>
2023-02-19 08:23:32 +00:00
Evgeny Poberezkin 92d9a1f9f2 ios: export localizations 2023-02-18 19:32:25 +00:00
Evgeny Poberezkin e5009a58df core: update simplexmq v4.4.1 2023-02-18 19:04:59 +00:00
Evgeny Poberezkin 35a1ce4903 core: separate core options to use in bots (#1937)
* core: separate core options to use in bots

* ci: install pkg-config for mac
2023-02-18 17:39:16 +00:00
Evgeny Poberezkin 7c4c627ee9 terminal: support multiline messages (as JSON strings) (#1936)
* terminal: support for multiline messages

* fix

* fix tests
2023-02-18 15:16:50 +00:00
Evgeny Poberezkin b7575ec01d core: encrypt/decrypt WebRTC frames (#1935)
* core: encrypt/decrypt WebRTC frames

* swift API

* add decrypt stub

* change name

* remove unused type

* move functions

* update cabal file

* copy bytes from encrypted string
2023-02-16 20:25:37 +00:00
Evgeny Poberezkin a0351d6f99 apps: update chat bots, readme (#1928)
* apps: update chat bots, readme

* CLI readme

* broadcast bot

* delete messages from non-publishers, better replies, support forwarding low-res images and links

* typo

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

* change

---------

Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
2023-02-14 07:57:27 +00:00
JRoberts 6f68840b3a 4.5.2: ios 125, Android 100 2023-02-10 11:43:14 +04:00
JRoberts 2eef858db1 ios: update core library 2023-02-10 11:10:35 +04:00
Evgeny Poberezkin 434315fb08 Merge branch 'stable' 2023-02-09 20:53:05 +00:00
Evgeny Poberezkin 9b495e576c readme: update groups 2023-02-09 20:52:44 +00:00
JRoberts 5405f44f54 core: 4.5.2.0 2023-02-09 16:34:44 +04:00
Stanislav Dmitrenko 53b05974c9 android: limit width + height in chat item view (#1920) 2023-02-09 12:00:33 +00:00
JRoberts 3530022152 ios, android: moderated item content types, CIDeleted type (#1919) 2023-02-09 15:10:35 +04:00
Stanislav Dmitrenko dc6bab7ae6 android: fixed broken autoscroll when a new message is coming (#1917)
* android: fixed broken autoscroll when a new message is coming

* spelling
2023-02-08 23:10:56 +00:00
Stanislav Dmitrenko c9b4ce457e android: prevent race when opening chat (#1914)
* android: prevent race when opening chat

* different way of doing things

* change

* change
2023-02-08 19:25:51 +00:00
JRoberts bd3325a889 core: show/keep message as moderated for moderator (#1916) 2023-02-08 22:29:36 +04:00
JRoberts 9e347484eb core: avoid using wickAckMessage handler on messages leading to connection deletion (#1915) 2023-02-08 21:23:53 +04:00
ishi_sama 894af0602d docs: french (#1905)
* Lang subfolder ; SERVER_fr.md ; fix minor typos

* fix the fix

* fix img src

* CONTRIBUTING_fr

* SQL_fr ; fix rev date

* WEBRTC_fr.md ; rev date

* CLI_fr ; rev date

* fix table content link and sum img src

* fix

* fixing the fix

am i dumb?

* polishing...

* README_fr.md (save)

* Update README_fr.md

* README_fr ; starting SIMPLEX_FR ; link fix

* update README (en+fr)

* Blog README_fr ; translators link

* typo

* SIMPLEX_fr ; fixes

* last fixes

* rename folder

* rename files/links

* update line

* remove ...

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-02-08 14:03:52 +00:00
Stanislav Dmitrenko aa6011a196 android: prevent race when changing active user (#1913) 2023-02-08 13:21:55 +00:00
Stanislav Dmitrenko f24035a99d android: prevent showing system alert after start (Android 13+) (#1909)
* android: prevent showing system alert after start (Android 13+)

* refactor

* added comments and renamed function

* rename

* rename

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-02-08 10:48:19 +00:00
Stanislav Dmitrenko c006b8150f ios: ask permission to open profiles (#1910) 2023-02-08 10:39:41 +00:00
Evgeny Poberezkin 9e4499de6d core: allow admins/owners delete member messages (#1869)
* core: allow admins/owners delete member messages

* allow message deletion to admins/owners

* deleted by types, schema

* check role

* fix test, view

* view, tests

* comment

* test timed deletion events

* refactor

* refactor

* refactor

---------

Co-authored-by: JRoberts <8711996+jr-simplex@users.noreply.github.com>
2023-02-08 11:08:53 +04:00
Evgeny Poberezkin a018e4a581 docs: translation guide (#1911)
* docs: translations

* add doc

* add images, corrections

* spellcheck

* link to translation guide from readme

* change image

* images
2023-02-07 21:57:51 +00:00
Stanislav Dmitrenko d29fd93ea7 docs: view files from private data directory of Android app (#1907)
* docs: view files from private data directory of Android app

* naming

* spelling

* spelling

* change
2023-02-07 15:17:10 +00:00
Stanislav Dmitrenko b30c7af3a3 mobile: including fully localized languages only (#1908)
* mobile: including fully localized languages only

* better place for code

* ios: including fully localized languages only

* Revert "ios: including fully localized languages only"

This reverts commit 42a0334d83.
2023-02-07 15:16:34 +00:00
Stanislav Dmitrenko 2798671d22 android: show alert when decode exception happens (#1906) 2023-02-07 12:08:54 +00:00
Stanislav Dmitrenko 0339b399f7 ios: don't go back when system alert appears (#1903) 2023-02-06 16:33:45 +00:00
Stanislav Dmitrenko d048962959 ios: Fixed screenshot size (#1902) 2023-02-06 16:00:29 +00:00
Stanislav Dmitrenko 4af91c4cae android: fixed size for exported QR code (#1901)
* android: fixed size for exported QR code

* border

* automatic version

* modifier

* make image instead of screenshot

* code folding

* don't use deprecated method

* function refactor

* dropped unneeded variable
2023-02-06 15:34:49 +00:00
Stanislav Dmitrenko 8a445ece90 ios: colored and clickable qr code with logo (#1885)
* ios: colored and clickable qr code with logo

* size of circle

* same padding as in android

* add padding to logo

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-02-06 08:45:56 +00:00
Stanislav Dmitrenko 5082f5b4a4 android: colored and clickable qr code with logo (#1884)
* android: colored and clickable qr code with logo

* save qr code as jpg for better quality

* bigger logo

* bigger logo (0.16f), low error correction (QR scans ok with up to 0.26f circle size)

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-02-06 08:45:40 +00:00
Evgeny Poberezkin c8fae0ec43 blog: typo 2023-02-05 23:46:42 +00:00
Evgeny Poberezkin d9a8d333f7 4.5.1: Android 99, iOS 124 2023-02-05 23:37:58 +00:00
Evgeny Poberezkin 73f8c543e3 translations (#1900)
* Translated using Weblate (Czech)

Currently translated at 100.0% (954 of 954 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 26.7% (255 of 954 strings)

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

* Translated using Weblate (French)

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

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

* Translated using Weblate (Japanese)

Currently translated at 8.5% (82 of 954 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 24.4% (219 of 895 strings)

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

* Translated using Weblate (Chinese (Traditional))

Currently translated at 1.8% (18 of 954 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 56.4% (538 of 953 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 1.6% (15 of 891 strings)

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

* Added translation using Weblate (Croatian)

* Added translation using Weblate (Croatian)

* Translated using Weblate (Dutch)

Currently translated at 63.6% (607 of 953 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 99.6% (950 of 953 strings)

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

* Translated using Weblate (Czech)

Currently translated at 66.1% (589 of 891 strings)

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

* Update translation files

Updated by "Remove blank strings" hook in Weblate.

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

* ios: import localizations

* ios: export localizations

---------

Co-authored-by: zenobit <zen@osowoso.xyz>
Co-authored-by: xqhjay <137847720@qq.com>
Co-authored-by: Ophiushi <Ophiushi@users.noreply.hosted.weblate.org>
Co-authored-by: tomato potato <4ryo49@protonmail.com>
Co-authored-by: Albert Bob <vicdorke@gmail.com>
Co-authored-by: Bdd55oo <giggzuv9z.eofjx@aleeas.com>
Co-authored-by: John m <jvanmanen@gmail.com>
Co-authored-by: Mehmed <pajazetovicmeho@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
2023-02-05 22:36:52 +00:00
Evgeny Poberezkin 14945a9296 ios: update core library 2023-02-05 22:30:03 +00:00
Evgeny Poberezkin 155ffd16ec core: 4.5.1.0 2023-02-05 22:06:27 +00:00
sh 1eb1e52912 call.ts: include udp stun/turn (#1892)
* call.ts: include udp stun/turn

* update JS

* show protocol, support TURNS

* mobile: add turn via UDP, remove protocol from view

* remove enums for protocol strings in ICE candidates

* 0.2.3

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-02-05 21:57:50 +00:00
Evgeny Poberezkin af173ee5c4 blog: v4.5 (#1886)
* blog: v4.5

* update post, images

* update readme, post
2023-02-05 13:53:41 +00:00
Evgeny Poberezkin 06a2f7e4da mobile: remove option "transfer images faster" (#1891) 2023-02-05 11:25:31 +00:00
Evgeny Poberezkin 958299784d core: update simplexmq (more strict transport host parser 2023-02-04 23:31:01 +00:00
Evgeny Poberezkin 49b6979ff0 core: update simplexmq (not to fail batch subscriptions), terminal: log contact errors with -c option (#1890) 2023-02-04 23:13:20 +00:00
Evgeny Poberezkin 3c493db613 mobile: use TCP for ICE requests of WebRTC calls (#1888)
* ios: support query string parameters in ICE server addresses

* android: support query params in ICE server address, add transport=TCP to default servers
2023-02-04 15:44:39 +00:00
Evgeny Poberezkin 86cc85b3a5 mobile: change default of "transfer images faster/inline" to off, mark as BETA (#1889)
* mobile: change default of "transfer images faster/inline" to off, mark as BETA

* ios: import localizations
2023-02-04 15:19:12 +00:00
Evgeny Poberezkin 0427d2e578 core: prevent failure to acknowledge a group message in case its parsing or saving fails (potential cause for stuck delivery) (#1887) 2023-02-04 12:25:11 +00:00
Evgeny Poberezkin c90d911d2a 4.5.0: Android 98, iOS 123 2023-02-03 15:19:26 +00:00
Evgeny Poberezkin 2473d14baa core: 4.5.0.4, update simplexmq 2023-02-03 11:32:32 +00:00
Evgeny Poberezkin a36f2147d8 mobile: current profile button in profile menu opens settings (#1882) 2023-02-03 11:22:17 +00:00
Evgeny Poberezkin 3837e92556 ios: fix advanced network config (#1881) 2023-02-03 11:17:56 +00:00
Evgeny Poberezkin 76505afff2 ios: import/export translations 2023-02-02 23:28:31 +00:00
Evgeny Poberezkin 89c9a01b20 mobile: translations (#1878)
* Translated using Weblate (Russian)

Currently translated at 100.0% (954 of 954 strings)

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

* Translated using Weblate (German)

Currently translated at 98.5% (882 of 895 strings)

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

* Translated using Weblate (Russian)

Currently translated at 100.0% (895 of 895 strings)

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

* Translated using Weblate (French)

Currently translated at 98.5% (882 of 895 strings)

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

* Translated using Weblate (Italian)

Currently translated at 98.5% (882 of 895 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 42.1% (402 of 954 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 3.8% (37 of 954 strings)

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

* Translated using Weblate (Italian)

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

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

* Translated using Weblate (German)

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

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

* Translated using Weblate (Czech)

Currently translated at 3.9% (38 of 954 strings)

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

* Translated using Weblate (Czech)

Currently translated at 100.0% (954 of 954 strings)

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

---------

Co-authored-by: John m <jvanmanen@gmail.com>
Co-authored-by: zenobit <zen@osowoso.xyz>
Co-authored-by: random r <epsilin@yopmail.com>
Co-authored-by: mlanp <github@lang.xyz>
2023-02-02 23:21:08 +00:00
Evgeny Poberezkin 68517cf852 android: disable chat preferences when chat is stopped (#1877) 2023-02-02 21:27:22 +00:00
Stanislav Dmitrenko fbbad55a0f android: Fix constraints of Compose that could crash the app (#1876)
* android: Fix constraints of Compose that could crash the app

* made constant
2023-02-02 19:46:33 +00:00
Stanislav Dmitrenko bcca27bfdb ios: show notifications for different users (#1874)
* ios: show notifications for different users

* refactore

* terminate background taks on chat item update

* refactor

* refactor2

* refactor3

* refactor 4

* refactor5

* fix chat item update in Android

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-02-02 16:09:36 +00:00
Evgeny Poberezkin c55a7692c5 4.5.0-beta.3: Android 97, iOS 122 2023-02-02 12:16:58 +00:00
Evgeny Poberezkin a8aa829e4c android: what is new in v45, ios: update texts 2023-02-02 11:20:37 +00:00
Evgeny Poberezkin d5af03ce18 ios: import/export localizations (#1873)
* ios: import localizations

* ios: export localizations
2023-02-02 10:43:18 +00:00
Evgeny Poberezkin 101ef7a81a translations: updates for user profiles, new languages (#1872)
* Added translation using Weblate (Spanish)

* Added translation using Weblate (Dutch)

* Added translation using Weblate (Dutch)

* Translated using Weblate (Russian)

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

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 11.0% (103 of 936 strings)

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

* Translated using Weblate (German)

Currently translated at 100.0% (936 of 936 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% (878 of 878 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 13.8% (130 of 936 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 14.9% (131 of 878 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 13.7% (129 of 936 strings)

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

* Added translation using Weblate (Japanese)

* Added translation using Weblate (Japanese)

* Added translation using Weblate (Hindi)

* Added translation using Weblate (Hindi)

* Added translation using Weblate (Czech)

* Added translation using Weblate (Czech)

* Added translation using Weblate (Polish)

* Added translation using Weblate (Polish)

* Added translation using Weblate (Portuguese (Brazil))

* Added translation using Weblate (Portuguese (Brazil))

* Added translation using Weblate (Spanish)

* Added translation using Weblate (Dutch)

* Added translation using Weblate (Dutch)

* Translated using Weblate (Russian)

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

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 11.0% (103 of 936 strings)

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

* Translated using Weblate (German)

Currently translated at 100.0% (936 of 936 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% (878 of 878 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 13.8% (130 of 936 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 14.9% (131 of 878 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 13.7% (129 of 936 strings)

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

* Added translation using Weblate (Japanese)

* Added translation using Weblate (Japanese)

* Added translation using Weblate (Hindi)

* Added translation using Weblate (Hindi)

* Added translation using Weblate (Czech)

* Added translation using Weblate (Czech)

* Added translation using Weblate (Polish)

* Added translation using Weblate (Polish)

* Added translation using Weblate (Portuguese (Brazil))

* Added translation using Weblate (Portuguese (Brazil))

* Translated using Weblate (French)

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

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 17.3% (162 of 936 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 16.2% (143 of 878 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 15.7% (147 of 936 strings)

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

* Translated using Weblate (Italian)

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

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 21.9% (206 of 939 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 20.7% (182 of 878 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (939 of 939 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 24.3% (229 of 939 strings)

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

* Translated using Weblate (Hindi)

Currently translated at 7.8% (74 of 939 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 31.2% (294 of 940 strings)

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

* Translated using Weblate (German)

Currently translated at 100.0% (940 of 940 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (940 of 940 strings)

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (940 of 940 strings)

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

* Translated using Weblate (Hindi)

Currently translated at 12.6% (119 of 940 strings)

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

* Translated using Weblate (Hindi)

Currently translated at 13.5% (127 of 940 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 22.7% (214 of 940 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 21.7% (191 of 878 strings)

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

* Translated using Weblate (Japanese)

Currently translated at 2.5% (24 of 940 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 23.6% (222 of 940 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 24.3% (214 of 878 strings)

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

---------

Co-authored-by: sith-on-mars <groguko36@pm.me>
Co-authored-by: mlanp <github@lang.xyz>
Co-authored-by: John m <jvanmanen@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: Ophiushi <Ophiushi@users.noreply.hosted.weblate.org>
Co-authored-by: random r <epsilin@yopmail.com>
Co-authored-by: Raman <translations.0l5zc@simplelogin.com>
Co-authored-by: tomato potato <4ryo49@protonmail.com>
2023-02-02 10:30:40 +00:00
Evgeny Poberezkin 71daeed81a ios: what is new in v4.5 2023-02-02 10:11:11 +00:00
Evgeny Poberezkin d44324eb4d readme: update weblate link 2023-02-02 09:47:39 +00:00
Evgeny Poberezkin 93d2ef66cf readme: translations (#1871) 2023-02-02 09:41:53 +00:00
Evgeny Poberezkin 8a78943e94 blog: v4.5 release announcement page 2023-02-02 09:00:34 +00:00
Evgeny Poberezkin d0f0013755 core: 4.5.0.3 2023-02-02 08:20:12 +00:00
Stanislav Dmitrenko f22ee1a6cf mobile: prevent WebRTC call failure/hanging when webview "failed" state happens before 30 sec timeout (#1866)
* mobile: do not end calls

* better way of continue connection and end with timeout

* making data classes instead of classes for making logs informative

* refactor

* update webrtc package version

* refactor

* fix

* clear conneciton timeout on disconnection

* refactor

* v0.2.1

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-02-01 19:23:28 +00:00
Evgeny Poberezkin 4a58ca60ac core: split tests (#1870) 2023-02-01 17:21:13 +00:00
Evgeny Poberezkin b206868730 core: add grop member role "observer" (#1868)
* core: add grop member role "observer"

* disable observer role until supported by most clients
2023-02-01 13:57:39 +00:00
Evgeny Poberezkin bd4c10b224 v4.5.0-beta.2: Android 96, iOS 121 2023-02-01 08:57:38 +00:00
Evgeny Poberezkin 46d15d1811 core: 4.5.0.2 2023-02-01 00:03:37 +00:00
Evgeny Poberezkin ea64be55e1 core: fix cancelling inline file transfer (#1867)
* core: fix cancelling inline file transfer

* fix test
2023-02-01 00:01:22 +00:00
Stanislav Dmitrenko c2cd58f63d android: adapted UserExists error (#1863)
* android: Adapted UserExists error

* updated texts

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-01-31 18:50:20 +00:00
Stanislav Dmitrenko f21fc76ced ios: adapted UserExists error (#1864)
* ios: adapted UserExists alert

* updated texts

* refactor

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-01-31 15:55:41 +00:00
Evgeny Poberezkin 13bd51b97d core: prevent making all users inactive when duplicate user is created (#1862)
* core: prevent making all users inactive when duplicate user is created

* skip async group test
2023-01-31 12:24:18 +00:00
Evgeny Poberezkin a1ed0a84b8 core: use port 7001 for test server (#1857)
* core: use port 7001 for test server

* enable only failing tests

* start/stop server for every test

* log message that failed to parse

* stop chat synchronously

* print call stack

* add HasCallStack

* increase test timeout

* add call stacks

* more call stacks

* fix test

* disable failing test

* add delay between the tests

* make delay more visible

* remove change in error message

* reduce test delay, increase timeout

* increase delay between the tests

* run each test with a database in a different folder

* folder name

* refactor

* update nix file, more stacks
2023-01-31 11:07:48 +00:00
Stanislav Dmitrenko 4815e447fa android: show alert instead of crash on user errors (#1861)
* android: show alert instead of crash on user errors

* show meaningful alert

* update alert messages

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-01-30 15:12:39 +00:00
Stanislav Dmitrenko d80cad57b6 android: require auth when opening users list (#1860)
* android: require auth when opening users list

* different logic in asking to auth

* unused code
2023-01-30 13:24:15 +00:00
Stanislav Dmitrenko a58be6ebb6 ios: limit number of items in console (#1859) 2023-01-30 12:07:06 +00:00
Stanislav Dmitrenko dfa0272065 android: user picker UI changes + share button (#1858)
* android: user picker UI changes + share button

* limit terminal items size in a different way
2023-01-30 11:19:26 +00:00
Evgeny Poberezkin 9723c47b25 4.5.0-beta.1: Android 95, iOS 120 2023-01-29 19:37:37 +00:00
Evgeny Poberezkin 3eb51eca58 core: v4.5.0.1 2023-01-29 18:52:38 +00:00
Evgeny Poberezkin 86151d4ec2 core: drop index causing slow queries (#1855)
* core: drop index causing slow queries

* update schema
2023-01-29 15:22:09 +00:00
Evgeny Poberezkin 717d05c4a3 android: fix bug when creating group with image 2023-01-29 12:01:19 +00:00
Evgeny Poberezkin 3c43c5d254 ios: import/export localizations 2023-01-28 17:48:37 +00:00
Evgeny Poberezkin 0bae260fae mobile: translations (#1850)
* Added translation using Weblate (Chinese (Simplified))

* Added translation using Weblate (Chinese (Simplified))

* Translated using Weblate (Chinese (Simplified))

Currently translated at 2.7% (25 of 911 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 2.6% (23 of 854 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 2.7% (25 of 911 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 7.0% (60 of 854 strings)

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

* 4.5-beta.0: Android 93, iOS 119

* Added translation using Weblate (Chinese (Simplified))

* Added translation using Weblate (Chinese (Simplified))

* Translated using Weblate (Chinese (Simplified))

Currently translated at 2.7% (25 of 911 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 2.6% (23 of 854 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 2.7% (25 of 911 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 7.0% (60 of 854 strings)

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

* Translated using Weblate (French)

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

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

* Translated using Weblate (Italian)

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

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

* Translated using Weblate (French)

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

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 10.7% (92 of 859 strings)

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

* Added translation using Weblate (Chinese (Traditional))

* Added translation using Weblate (Chinese (Traditional))

* Added translation using Weblate (English (United Kingdom))

* Added translation using Weblate (Spanish)

* Translated using Weblate (Chinese (Simplified))

Currently translated at 10.1% (95 of 936 strings)

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

* remove UK English file

---------

Co-authored-by: sith-on-mars <groguko36@pm.me>
Co-authored-by: Ophiushi <Ophiushi@users.noreply.hosted.weblate.org>
Co-authored-by: random r <epsilin@yopmail.com>
Co-authored-by: Albert Bob <vicdorke@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
2023-01-28 17:44:12 +00:00
Evgeny Poberezkin e694848cd5 4.5-beta.0: Android 94, iOS 119 2023-01-28 17:11:32 +00:00
Evgeny Poberezkin f5f61c5806 core: update simplexmq, v4.5.0.0 2023-01-28 13:59:46 +00:00
Stanislav Dmitrenko 96c1c1d439 android: drafts (#1849)
* android: drafts

* empty line

* delete unused voice files

* finish recording properly

* refactor

* fix

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-01-28 13:35:31 +00:00
Evgeny Poberezkin 3e560278b6 ios: update chat previews, show filename in drafts (#1847)
* ios: update chat previews, show filename in drafts

* save and restore images/file/voice for draft

* refactor image

* it was a wrong value

* use param label

* proper stop of voice recording

* safe draft logic

* different way of finishing recording

* keep condition

* refactor

* fix live

* fix

* refactor

* fix

* simplify

* add space after filename in draft

---------

Co-authored-by: Avently <7953703+avently@users.noreply.github.com>
2023-01-27 22:09:39 +00:00
Evgeny Poberezkin 6e131e0bad ios: disable current user profile button 2023-01-27 12:54:42 +00:00
Stanislav Dmitrenko bd158f3b0d android: user-specific settings (#1848)
* android: multiuser-peruser

* padding

* bigger padding

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-01-27 12:28:44 +00:00
M Sarmad Qadeer a96fb2f8d1 website: improvements to design (#1405)
* updated the design of join simplex

* updated the design of Comparison section

* updated the design of Simplex explained

* updated the design of Simplex network

* updated the design of private section

* updated the design of features section

* updated the design of unique section

* updated the design of privacy matters

* added improvements in design

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-01-27 12:07:22 +00:00
Evgeny Poberezkin 148261a1ee ios: allow to reply in another chat without losing draft 2023-01-26 23:28:56 +00:00
Stanislav Dmitrenko 22b7aa90b2 android: multiuser-notifications (#1846) 2023-01-26 23:00:54 +00:00
Stanislav Dmitrenko 88d9e70ef8 android: mutliuser-calls (#1845)
* android: mutliuser-calls

* tint color of icon

* userId from function

* better line

* missing question

* bigger avatar

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-01-26 21:36:49 +00:00
Evgeny Poberezkin a0cfc19063 Merge pull request #1844 from simplex-chat/av/multiuser-ui
android: multiusers-profilemanager
2023-01-26 20:49:02 +00:00
Avently 451aab46dc disable deleting the last user 2023-01-26 23:46:10 +03:00
Avently 3b3db562cd added description 2023-01-26 23:14:33 +03:00
Avently 15ed6ea4a6 android: multiusers-profilemanager 2023-01-26 21:23:52 +03:00
Evgeny Poberezkin 9f661ff4e2 Merge pull request #1698 from simplex-chat/users
support multiple user profiles
2023-01-26 10:14:22 +00:00
Evgeny Poberezkin 52c39c9641 Merge branch 'master' into users 2023-01-25 23:31:18 +00:00
Evgeny Poberezkin 0dab486ac8 readme: update group links 2023-01-25 22:48:08 +00:00
Evgeny Poberezkin d640f4a5d5 readme: remove broken twitter badge 2023-01-25 21:04:19 +00:00
Stanislav Dmitrenko 1c47bfbf44 android: better user picker layout (#1842)
* android: multiuser-fixes

* update paddings

* progressIndicator

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-01-25 20:43:02 +00:00
Stanislav Dmitrenko db3fc4ee7b android: multiuser-userpicker (#1839)
* android: multiuser-userpicker

* sizes of buttons

* update paddings

* change names

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-01-25 15:30:30 +00:00
JRoberts 74df35d3b0 core: add multiple users tests for subscription, chat item expiration, timed messages (#1840) 2023-01-25 19:29:09 +04:00
Evgeny Poberezkin c01c629f73 mobile: use GMT timezone in filenames to prevent leaking user location (#1837)
* ios: use GMT timezone in filenames to prevent leaking user location

* android: use GMT timestamp in generated file names
2023-01-25 11:48:54 +00:00
Evgeny Poberezkin 25e4a1e86d ios: fix layout of voice message (#1836)
* ios: fix layout of voice message

* fix layout

* prevent translations
2023-01-25 10:18:02 +00:00
Evgeny Poberezkin e27013071b ios: preserve message draft in the latest chat only (#1834)
* ios: preserve message draft in the latest chat only

* show attachment icon and formatting in draft

* button to remove message text

* show icon for active draft, refactor

* add voice message duration to draft
2023-01-25 08:35:25 +00:00
Stanislav Dmitrenko 2679bc2e94 ios: enable swipe to go back from chat to list (#1824)
* ios: Testing workaround of a crash

* another try

* complete

* added file

* enable swipe to go back from ChatView

* Revert "enable swipe to go back from ChatView"

This reverts commit 22de79505c.

* ios: enable swipe to go back from ChatView

* remove title change

* remove unused

* remove unused variable

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-01-24 19:24:46 +00:00
Evgeny Poberezkin 93ab713748 ios: choose user deletion mode (#1833)
* ios: choose user deletion mode

* update text

* refactor, disable button

* darker profile icon colors

* do not delete active user if changing user failed
2023-01-24 19:00:30 +00:00
JRoberts bc1d86e303 core: send agent DEL events to view (#1832) 2023-01-24 20:07:35 +04:00
Evgeny Poberezkin b386346cf1 core: update syntact for /_delete (#1831) 2023-01-24 16:00:32 +00:00
JRoberts b6db41dd50 update simplexmq (complete) 2023-01-24 18:46:02 +04:00
Stanislav Dmitrenko 6bca013e67 android: multiuser-api (#1829)
* android: multiuser-api

* add line in try-catch block

* changed lines position

* when -> if

* take condition outside

* mutable version of objects and usage of a new function

* changed additional places in code

* added toMap() so state will be updated

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-01-24 14:29:20 +00:00
Evgeny Poberezkin e538826cd8 Merge branch 'master' into users 2023-01-24 14:09:38 +00:00
JRoberts a7a56ea1d9 core: use batch delete api when deleting unused group contacts (#1830) 2023-01-24 17:58:08 +04:00
Evgeny Poberezkin 77d0f70270 update simplexmq 2023-01-24 13:39:12 +00:00
JRoberts 2a20f78877 core: use batch connection deletion api (#1814) 2023-01-24 16:24:34 +04:00
Evgeny Poberezkin b027708828 4.4.4: Android 92, iOS 118 2023-01-23 20:17:52 +00:00
Evgeny Poberezkin a0bf298b66 Merge branch 'master' into users 2023-01-23 18:45:52 +00:00
Evgeny Poberezkin e3b22d83ad Merge branch 'master' into users 2023-01-23 18:45:24 +00:00
JRoberts ab4e4e1db9 core: test cancelling inline file transfer (#1827)
* core: test cancelling inline file transfer

* tests
2023-01-23 18:27:44 +00:00
Stanislav Dmitrenko a393bc8163 ios: Testing workaround of a crash (#1789)
* ios: Testing workaround of a crash

* another try

* complete

* added file

* enable swipe to go back from ChatView

* Revert "enable swipe to go back from ChatView"

This reverts commit 22de79505c.

* refactor

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-01-23 18:17:33 +00:00
Evgeny Poberezkin 3bc4fd222c core: fix cancelling sending inline files (#1826)
* core: fix cancelling sending inline files

* add comments

* typos

Co-authored-by: JRoberts <8711996+jr-simplex@users.noreply.github.com>

Co-authored-by: JRoberts <8711996+jr-simplex@users.noreply.github.com>
2023-01-23 21:17:42 +04:00
JRoberts 1b01dcec6d core: fix inline file transfer - optional connection ids in RcvFileInfo, update rcv_file_inline on accept (#1823)
* core: optional connection id in RcvFileInfo

* query

* check snd cancel

* Revert "check snd cancel"

This reverts commit f16651345d.

* update rcv_file_inline
2023-01-23 15:55:19 +00:00
Stanislav Dmitrenko 1d5c361b9a ios: restore scroll and update user profile in user profile menu (#1811)
* ios: Small UserPicker fixes

* update scroll

* update current user and update users list

* refactor

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-01-23 15:48:29 +00:00
Stanislav Dmitrenko 4cd396a0d2 ios: Multiuser calls (#1800)
* ios: Multiuser calls

* counter update on badge

* padding before profile info in call view

* underline in name

* change after merge

* do not show Simplex Info button if users already created

* unread counter

* do not increase badge counter when chat has disabled notifications

* update incoming call

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-01-23 13:20:58 +00:00
Evgeny Poberezkin bcc80be8e9 Merge branch 'master' into users 2023-01-22 23:08:53 +00:00
Evgeny Poberezkin 5d12217eab 4.4.4: Android 91, iOS 117 2023-01-22 19:30:19 +00:00
Evgeny Poberezkin 4b0046a60b mobile: show version information (#1820)
* mobile: show version information

* export localizations
2023-01-22 18:34:01 +00:00
Evgeny Poberezkin 114b76e3f8 core: update version response (#1819)
* core: update version response

* add simplexmq commit and version to version info

* refactor
2023-01-22 15:16:45 +00:00
Evgeny Poberezkin c72aa5d074 Merge branch 'master' into users 2023-01-21 23:14:26 +00:00
Evgeny Poberezkin 2e9882b0bd Merge branch 'master' into users 2023-01-21 23:00:06 +00:00
Evgeny Poberezkin 8ff8f9d695 core: add build timestamp to version information (#1816) 2023-01-21 22:56:33 +00:00
Evgeny Poberezkin 1e3c2024bb android: add localization of "offered/cancelled" feature items, closes #1766 (#1815) 2023-01-21 16:47:26 +00:00
Evgeny Poberezkin 8bec0004cc mobile: UI to choose transport isolation mode (#1813)
* mobile: UI to choose transport isolation mode

* typo

Co-authored-by: JRoberts <8711996+jr-simplex@users.noreply.github.com>

* ios: update alerts

Co-authored-by: JRoberts <8711996+jr-simplex@users.noreply.github.com>
2023-01-21 16:05:09 +00:00
JRoberts c337a6888d core: delete previous contact calls when receiving a new one (#1812) 2023-01-21 16:40:24 +04:00
JRoberts e72d4638d2 core: exlude muted chats from user unread count (#1810) 2023-01-20 20:48:24 +04:00
JRoberts 7dd4dc3b40 core: support accepting contact requests for non active users (for accepting via notification) (#1809)
* core: support accepting contact requests for non active users (for accepting via notification)

* getContactRequest'
2023-01-20 17:55:57 +04:00
JRoberts 980c7a9ddd ios: use agent connection id as key for network statuses map (#1808) 2023-01-20 17:35:39 +04:00
Evgeny Poberezkin cb5f26d354 ios: only show menu if there is more than 1 user, do not show unread count (only badge) 2023-01-20 13:17:41 +00:00
Evgeny Poberezkin 04d886e546 ios: user profiles view, per-user settings (#1801)
* ios: user profiles view, per-user settings

* remove comment

* bold profile name
2023-01-20 12:38:38 +00:00
Evgeny Poberezkin ed12ccaac2 ios: update library 2023-01-20 12:28:45 +00:00
Evgeny Poberezkin e9e9286fbb Merge branch 'master' into users 2023-01-20 12:22:29 +00:00
JRoberts ef15dca0b4 core: don't filter out non active user connections on UP & DOWN agent events; use agent connection id instead of db connection id for ContactRef (#1807) 2023-01-20 15:02:27 +04:00
JRoberts 396b3ae639 ios: maintain connections network statuses map separately from chats (allows to keep track of network statuses for all users) (#1803) 2023-01-20 14:56:05 +04:00
Evgeny Poberezkin 69ca731641 core: process push notifications for any user (#1806)
* core: process push notifications for any user

* return regardless

* refactor

* more refactor
2023-01-20 10:48:25 +00:00
Evgeny Poberezkin 006a30e65c update simplexmq 2023-01-19 19:41:19 +00:00
JRoberts cf4105e256 core: add connection id to ContactRef (#1798) 2023-01-19 20:54:00 +04:00
Stanislav Dmitrenko ad6aa10cd2 ios: Multiusers feature continue (#1793)
* ios: Multiusers feature continue

* Logging of user in responses

* UserId

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

* Undo ugly user inclusion into functions.  Now it's in backend

* Do not set active user if it's unchanged

* Blank line

* if

* Change active user function

* refactor

* refactor

Co-authored-by: JRoberts <8711996+jr-simplex@users.noreply.github.com>

* Alert

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
Co-authored-by: JRoberts <8711996+jr-simplex@users.noreply.github.com>
2023-01-19 16:22:56 +00:00
Stanislav Dmitrenko ba29d0242e core: add user to RcvCallInvitation (#1797)
* core: Include user into RcvCallInvitation

* update build

* parens

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-01-19 16:00:41 +00:00
JRoberts ca64ed9784 core: option to reuse servers for new user; support for users to configure same smp servers (add user_id to smp_servers UNIQUE constraint) (#1792) 2023-01-18 18:49:56 +04:00
JRoberts a227e21fcf core: support user deletion (#1788)
* core: support user deletion

* doSendCancel

* Apply suggestions from code review

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

* sendCancel

* refactor

* error to view

* refactor

* refactor

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-01-18 17:08:48 +04:00
Evgeny Poberezkin 84237f79fc core: refactor (#1764) 2023-01-18 10:20:55 +00:00
Stanislav Dmitrenko 153f80fe64 ios: menu to switch active user profile (#1758)
* ios: User chooser UI

* Change

* Changes

* update view

* fix layout/refactor

* fix preview

* wider menu, update label

* hide Your profiles button

* Clickable background that hides userChooser

* No click listener

* Better animation

* Disabled scrolling for small number of items

* Separated scrollview and buttons

* No transition

* Re-indent

* Limiting width by the longest label

* UserManagerView

* Adapted API

* Hide user chooser after selection

* Top counter,  users refactor

* Padding

* use VStack to fix layout bug

* eol

* rename: rename to getUserChatData

* update layout

* s/semibold/medium

* remove SettingsButton view

* rename

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-01-17 17:47:37 +00:00
JRoberts a668bd5736 core: cleanup obsolete chat item deletion code (see #1625) (#1787) 2023-01-17 16:58:36 +04:00
JRoberts e8cab01c03 core: update simplexmq (fkey indexes) (#1786) 2023-01-17 16:41:19 +04:00
JRoberts 3f72633d22 Merge branch 'master' into users 2023-01-17 15:58:02 +04:00
JRoberts 5c7ad0926c core: add missing fkey indexes (#1785) 2023-01-17 15:45:37 +04:00
JRoberts 2f39cfd86f core: support marking chat items read for any user (#1784) 2023-01-17 13:08:51 +04:00
JRoberts 2fdc23274d core: return user unread counts on ListUsers command (#1763)
* core: return user unread counts on ListUsers command

* split

* tests

* refactor

* viewUserInfo

* refactor

* remove omit nothing

* corrections

* fix

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-01-16 18:57:31 +00:00
Evgeny Poberezkin 91a39cae23 core: fix error handling (#1761)
* core: fix error handling

* fix tests
2023-01-16 17:25:06 +00:00
Evgeny Poberezkin 882966d5d3 ios: update network config (#1760) 2023-01-16 15:10:16 +00:00
JRoberts 3ed5e6e50b core: support receiving file by id for any user (not only current) (#1759) 2023-01-16 17:51:25 +04:00
JRoberts df6cec6a32 core: update simplexmq (session mode, users commands) (#1757) 2023-01-16 17:00:24 +04:00
JRoberts 24c47657f4 Merge branch 'master' into users 2023-01-16 16:37:13 +04:00
JRoberts cf6afb7687 Merge branch 'master' into users 2023-01-16 16:24:38 +04:00
JRoberts 9dc6c1327f core: manage calls for all users (#1748) 2023-01-16 15:06:03 +04:00
JRoberts a040fa65bb core: run cleanup for all users (#1746) 2023-01-14 19:21:10 +04:00
JRoberts 9fc26ca799 core: start chat item expiration thread for new users (#1745) 2023-01-14 17:52:40 +04:00
JRoberts 6e0addbea3 core: add user to CRSmpTestResult response (#1744) 2023-01-14 15:45:42 +04:00
JRoberts e452edb781 core: subscribe all users (#1743) 2023-01-14 15:45:13 +04:00
JRoberts 9290fcc6b2 core: set active prompt to none when changing current user (#1738) 2023-01-13 21:01:36 +04:00
JRoberts 0c3d643408 core: expire chat items for all users (#1737) 2023-01-13 21:01:26 +04:00
JRoberts cccdcef914 core: add delays to tests to prevent output races (#1736) 2023-01-13 16:26:55 +04:00
Evgeny Poberezkin e63e158b2d core: refactor withUserId (#1735)
* refactor withUserId

* update

* more
2023-01-13 16:24:54 +04:00
JRoberts 892b91e958 core: update simplexmq (subscribe users in different sessions) (#1734) 2023-01-13 15:14:46 +04:00
JRoberts fb04108b11 Merge branch 'master' into users 2023-01-13 14:19:21 +04:00
JRoberts 424328b9d1 core: agent users (#1727) 2023-01-13 13:54:07 +04:00
JRoberts 7323bb4333 Merge branch 'master' into users 2023-01-11 18:38:55 +04:00
JRoberts 41e873d5ca core: multiple users view, tests (#1710) 2023-01-11 11:00:28 +04:00
JRoberts ad1b091b18 Merge branch 'master' into users 2023-01-09 17:02:38 +04:00
JRoberts bb0482104c core, ios, android: add UserId to api commands (#1696) 2023-01-05 20:38:31 +04:00
JRoberts fa9e0086f6 core: multiple users api (#1679)
* api

* UCR

* Revert "UCR"

This reverts commit 1f98d25192.

* comment

* events User

* events in api User

* CRActiveUser in APISetActiveUser

* process message with/without connection

* refactor

* mute error

* user in api responses

* name

* lost response

* user in CRChatCmdError

* compiles

* user in CRChatError

* -- UserId

* mute unused warning

* catch in withUser

* remove comment

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-01-04 21:06:28 +04:00
816 changed files with 143480 additions and 15268 deletions
+68
View File
@@ -0,0 +1,68 @@
name: Bug
description: File a bug report/issue
title: "[Bug]: "
labels: ["type:bug", "type:triage"]
body:
- type: checkboxes
attributes:
label: Is there an existing issue for this?
description: Please search to see if an issue already exists for the bug you encountered.
options:
- label: I have searched the existing issues
required: true
- type: dropdown
attributes:
label: Platform
description: Multiple selections are possible.
multiple: true
options:
- Linux
- Mac
- Windows
- Android
- iOS
validations:
required: true
- type: input
attributes:
label: OS version
description: Specify the OS version
placeholder: ex. Android 12, Ubuntu 20.04
validations:
required: true
- type: input
attributes:
label: App version
description: Specify the SimpleX version
placeholder: ex. 4.3.2
validations:
required: true
- type: textarea
attributes:
label: Current Behavior
description: A concise description of what you're experiencing.
placeholder: Bug happened!
validations:
required: true
- type: textarea
attributes:
label: Expected Behavior
description: A concise description of what you expected to happen.
placeholder: No bug should happen!
validations:
required: true
- type: textarea
attributes:
label: Steps To Reproduce
description: Steps to reproduce the behavior.
placeholder: |
1. Go to ...
3. Click on ...
4. See error...
validations:
required: true
- type: textarea
attributes:
label: Relevant log output
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
render: shell
+1
View File
@@ -0,0 +1 @@
blank_issues_enabled: true
+40
View File
@@ -0,0 +1,40 @@
name: Feature
description: Suggest your feature
title: "[Feature]: "
labels: ["type:enhancement", "type:triage"]
body:
- type: checkboxes
attributes:
label: Is there an existing issue for this?
description: Please search to see if an issue already exists for the bug you encountered.
options:
- label: I have searched the existing issues
required: true
- type: dropdown
attributes:
label: Platform
description: Multiple selections are possible. If selected input is "all", this considered to be a general feature.
multiple: true
options:
- Linux
- Mac
- Windows
- Android
- iOS
- all
validations:
required: true
- type: input
attributes:
label: App version
description: Specify the SimpleX version
placeholder: ex. 4.3.2
validations:
required: false
- type: textarea
attributes:
label: Feature
description: Describe the feature you would like to see added
placeholder: SimpleX Chat should make me coffee!
validations:
required: true
+16
View File
@@ -0,0 +1,16 @@
name: Question
description: Ask your question
title: "[Q]: "
labels: ["type:question", "type:triage"]
body:
- type: markdown
attributes:
value: |
Generally, we encourage you to ask questions in our [official group](https://simplex.chat/invitation/#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3Dsimplex:/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D), but you can do it anyway :)
- type: textarea
attributes:
label: Question
description: Please ask your question in plain english.
placeholder: Is SimpleX - chat?
validations:
required: true
+10 -6
View File
@@ -5,7 +5,7 @@ on:
branches: branches:
- master - master
- stable - stable
- sqlcipher - users
tags: tags:
- "v*" - "v*"
pull_request: pull_request:
@@ -52,9 +52,9 @@ jobs:
- os: ubuntu-20.04 - os: ubuntu-20.04
cache_path: ~/.cabal/store cache_path: ~/.cabal/store
asset_name: simplex-chat-ubuntu-20_04-x86-64 asset_name: simplex-chat-ubuntu-20_04-x86-64
- os: ubuntu-18.04 - os: ubuntu-22.04
cache_path: ~/.cabal/store cache_path: ~/.cabal/store
asset_name: simplex-chat-ubuntu-18_04-x86-64 asset_name: simplex-chat-ubuntu-22_04-x86-64
- os: macos-latest - os: macos-latest
cache_path: ~/.cabal/store cache_path: ~/.cabal/store
asset_name: simplex-chat-macos-x86-64 asset_name: simplex-chat-macos-x86-64
@@ -91,8 +91,12 @@ jobs:
echo " extra-lib-dirs: /usr/local/opt/openssl@1.1/lib" >> cabal.project.local echo " extra-lib-dirs: /usr/local/opt/openssl@1.1/lib" >> cabal.project.local
echo " flags: +openssl" >> cabal.project.local echo " flags: +openssl" >> cabal.project.local
- name: Install pkg-config for Mac
if: matrix.os == 'macos-latest'
run: brew install pkg-config
- name: Unix prepare cabal.project.local for Ubuntu - name: Unix prepare cabal.project.local for Ubuntu
if: matrix.os == 'ubuntu-20.04' || matrix.os == 'ubuntu-18.04' if: matrix.os == 'ubuntu-20.04' || matrix.os == 'ubuntu-22.04'
shell: bash shell: bash
run: | run: |
echo "ignore-project: False" >> cabal.project.local echo "ignore-project: False" >> cabal.project.local
@@ -108,8 +112,8 @@ jobs:
echo "::set-output name=bin_path::$(cabal list-bin simplex-chat)" echo "::set-output name=bin_path::$(cabal list-bin simplex-chat)"
- name: Unix test - name: Unix test
if: matrix.os != 'windows-latest' && matrix.os != 'ubuntu-20.04' if: matrix.os != 'windows-latest'
timeout-minutes: 10 timeout-minutes: 30
shell: bash shell: bash
run: cabal test --test-show-details=direct run: cabal test --test-show-details=direct
+5 -1
View File
@@ -42,12 +42,15 @@ stack.yaml.lock
# Temporary test files # Temporary test files
tests/tmp tests/tmp
tests/tmp*
logs/ logs/
*.devcontainer
# for website # for website
website/node_modules/ website/node_modules/
website/src/blog/ website/src/blog/
website/translations.json
website/src/_data/supported_languages.json
website/src/img/images/ website/src/img/images/
website/src/images/ website/src/images/
# Generated files # Generated files
@@ -72,3 +75,4 @@ website/package-lock.json
# Ignore test files # Ignore test files
website/.cache website/.cache
website/test/stubs-layout-cache/_includes/*.js website/test/stubs-layout-cache/_includes/*.js
apps/android/app/release
+159 -84
View File
@@ -1,13 +1,28 @@
<img src="images/simplex-chat-logo.svg" alt="SimpleX logo" width="100%">
# SimpleX - the first messaging platform that has no user identifiers of any kind - 100% private by design!
[![build](https://github.com/simplex-chat/simplex-chat/actions/workflows/build.yml/badge.svg?branch=stable)](https://github.com/simplex-chat/simplex-chat/actions/workflows/build.yml) [![build](https://github.com/simplex-chat/simplex-chat/actions/workflows/build.yml/badge.svg?branch=stable)](https://github.com/simplex-chat/simplex-chat/actions/workflows/build.yml)
[![GitHub downloads](https://img.shields.io/github/downloads/simplex-chat/simplex-chat/total)](https://github.com/simplex-chat/simplex-chat/releases) [![GitHub downloads](https://img.shields.io/github/downloads/simplex-chat/simplex-chat/total)](https://github.com/simplex-chat/simplex-chat/releases)
[![GitHub release](https://img.shields.io/github/v/release/simplex-chat/simplex-chat)](https://github.com/simplex-chat/simplex-chat/releases) [![GitHub release](https://img.shields.io/github/v/release/simplex-chat/simplex-chat)](https://github.com/simplex-chat/simplex-chat/releases)
[![Join on Reddit](https://img.shields.io/reddit/subreddit-subscribers/SimpleXChat?style=social)](https://www.reddit.com/r/SimpleXChat) [![Join on Reddit](https://img.shields.io/reddit/subreddit-subscribers/SimpleXChat?style=social)](https://www.reddit.com/r/SimpleXChat)
[![Follow on Mastodon](https://img.shields.io/mastodon/follow/108619463746856738?domain=https%3A%2F%2Fmastodon.social&style=social)](https://mastodon.social/@simplex) [![Follow on Mastodon](https://img.shields.io/mastodon/follow/108619463746856738?domain=https%3A%2F%2Fmastodon.social&style=social)](https://mastodon.social/@simplex)
[![Follow on Twitter](https://img.shields.io/twitter/follow/SimpleXChat?style=social)](https://twitter.com/SimpleXChat)
| 30/03/2023 | EN, [FR](/docs/lang/fr/README.md), [CZ](/docs/lang/cs/README.md) |
<img src="images/simplex-chat-logo.svg" alt="SimpleX logo" width="100%">
# SimpleX - the first messaging platform that has no user identifiers of any kind - 100% private by design!
[<img src="./images/trail-of-bits.jpg" height="100">](http://simplex.chat/blog/20221108-simplex-chat-v4.2-security-audit-new-website.html) &nbsp;&nbsp;&nbsp; [<img src="./images/privacy-guides.jpg" height="80">](https://www.privacyguides.org/en/real-time-communication/#simplex-chat) &nbsp;&nbsp;&nbsp; [<img src="./images/kuketz-blog.jpg" height="80">](https://www.kuketz-blog.de/simplex-eindruecke-vom-messenger-ohne-identifier/)
## Welcome to SimpleX Chat!
1. 📲 [Install the app](#install-the-app).
2. ↔️ [Connect to the team](#connect-to-the-team-via-the-app) and [join user groups](#join-user-groups).
3. 🤝 [Make a private connection](#make-a-private-connection) with a friend.
4. 🔤 [Help translating SimpleX Chat](#help-translating-simplex-chat).
5. ⚡️ [Contribute](#contribute) and [help us with donations](#help-us-with-donations).
[Learn more about SimpleX Chat](#contents).
## Install the app
[<img src="https://github.com/simplex-chat/.github/blob/master/profile/images/apple_store.svg" alt="iOS app" height="42">](https://apps.apple.com/us/app/simplex-chat/id1605771084) [<img src="https://github.com/simplex-chat/.github/blob/master/profile/images/apple_store.svg" alt="iOS app" height="42">](https://apps.apple.com/us/app/simplex-chat/id1605771084)
&nbsp; &nbsp;
@@ -23,9 +38,109 @@
- 🔐 Double ratchet end-to-end encryption, with additional encryption layer. - 🔐 Double ratchet end-to-end encryption, with additional encryption layer.
- 📱 Mobile apps for Android ([Google Play](https://play.google.com/store/apps/details?id=chat.simplex.app), [APK](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex.apk)) and [iOS](https://apps.apple.com/us/app/simplex-chat/id1605771084). - 📱 Mobile apps for Android ([Google Play](https://play.google.com/store/apps/details?id=chat.simplex.app), [APK](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex.apk)) and [iOS](https://apps.apple.com/us/app/simplex-chat/id1605771084).
- 🚀 [TestFlight preview for iOS](https://testflight.apple.com/join/DWuT2LQu) with the new features 1-2 weeks earlier - **limited to 10,000 users**! - 🚀 [TestFlight preview for iOS](https://testflight.apple.com/join/DWuT2LQu) with the new features 1-2 weeks earlier - **limited to 10,000 users**!
- 🖥 Available as a terminal (console) app / CLI on Linux, MacOS, Windows. - 🖥 Available as a terminal (console) [app / CLI](#zap-quick-installation-of-a-terminal-app) on Linux, MacOS, Windows.
**NEW**: Security audit by [Trail of Bits](https://www.trailofbits.com/about), the [new website](https://simplex.chat) and v4.2 released! [See the announcement](./blog/20221108-simplex-chat-v4.2-security-audit-new-website.md) ## Connect to the team via the app
- to ask any questions
- to suggest any improvements
- to share anything relevant
## Join user groups
**Please note**: The groups below are created for the users to be able to ask questions, make suggestions and ask questions about SimpleX Chat only.
You also can:
- criticize the app, and make comparisons with other messengers.
- share new messengers you think could be interesting for privacy, as long as you don't spam.
- share some privacy related publications, infrequently.
- having preliminary approved with the admin in direct message, share the link to a group you created.
You must:
- be polite to other users
- avoid spam (too frequent messages, even if they are relevant)
- avoid any personal attacks or hostility.
- avoid sharing any content that is not relevant to the above (that includes, but is not limited to, discussing politics or any aspects of society other than privacy, security, technology and communications, sharing any content that may be found offensive by other users, etc.).
Messages not following these rules will be deleted, the right to send messages may be revoked, and the access to the new members to the group may be temporarily restricted, to prevent re-joining under a different name - our imperfect group moderation does not have a better solution at the moment.
You can join an English-speaking users group if you want to ask any questions: [#SimpleX-Group-3](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2Fp-j-D_PrY2UMDchFHEUtbSES0nmzCnvD%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEA3gBfMjB_GDEmKQwjNdqGbnX91yfuZ7nRJgQijsx5Khc%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%2262MvNZ_Ec2mmlS8V0QNtLQ%3D%3D%22%7D)
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-FR](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2Fhpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg%3D%40smp5.simplex.im%2FvIHQDxTor53nwnWWTy5cHNwQQAdWN5Hw%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAPdgK1eBnETmgiqEQufbUkydKBJafoRx4iRrtrC2NAGc%253D%26srv%3Djjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%221FyUryBPza-1ZFFE80Ekbg%3D%3D%22%7D) (French-speaking), [\#SimpleX-RU](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FXZyt3hJmWsycpN7Dqve_wbrAqb6myk1R%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAMFVIoytozTEa_QXOgoZFq_oe0IwZBYKvW50trSFXzXo%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22xz05ngjA3pNIxLZ32a8Vxg%3D%3D%22%7D) (Russian-speaking), [\#SimpleX-IT](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2F0weR-ZgDUl7ruOtI_8TZwEsnJP6UiImA%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAq4PSThO9Fvb5ydF48wB0yNbpzCbuQJCW3vZ9BGUfcxk%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22e-iceLA0SctC62eARgYDWg%3D%3D%22%7D) (Italian-speaking).
You can join either by opening these links in the app or by opening them in a desktop browser and scanning the QR code.
## Make a private connection
You need to share a link with your friend or scan a QR code from their phone, in person or during a video call, to make a connection and start messaging.
The channel through which you share the link does not have to be secure - it is enough that you can confirm who sent you the message and that your SimpleX connection is established.
<img src="https://github.com/simplex-chat/.github/blob/master/profile/images/app1.png" alt="Make a private connection" height="360"> <img src="https://github.com/simplex-chat/.github/blob/master/profile/images/arrow.png" height="360"> <img src="https://github.com/simplex-chat/.github/blob/master/profile/images/app2.png" alt="Conversation" height="360"> <img src="https://github.com/simplex-chat/.github/blob/master/profile/images/arrow.png" height="360"> <img src="https://github.com/simplex-chat/.github/blob/master/profile/images/app3.png" alt="Video call" height="360">
After you connect, you can [verify connection security code](./blog/20230103-simplex-chat-v4.4-disappearing-messages.md#connection-security-verification).
## User guide (NEW)
Read about the app features and settings in the new [User guide](./docs/guide/README.md).
## Help translating SimpleX Chat
Thanks to our users and [Weblate](https://hosted.weblate.org/engage/simplex-chat/), SimpleX Chat apps, website and documents are translated to many other languages.
Join our translators to help SimpleX grow!
|locale|language |contributor|[Android](https://play.google.com/store/apps/details?id=chat.simplex.app) and [iOS](https://apps.apple.com/us/app/simplex-chat/id1605771084)|[website](https://simplex.chat)|Github docs|
|:----:|:-------:|:---------:|:---------:|:---------:|:---------:|
|🇬🇧 en|English | |✓|✓|✓|✓|
|ar|العربية |[jermanuts](https://github.com/jermanuts)||[![website](https://hosted.weblate.org/widgets/simplex-chat/ar/website/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/website/ar/)||
|🇨🇿 cs|Čeština |[zen0bit](https://github.com/zen0bit)|[![android app](https://hosted.weblate.org/widgets/simplex-chat/cs/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/cs/)<br>[![ios app](https://hosted.weblate.org/widgets/simplex-chat/cs/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/cs/)|[![website](https://hosted.weblate.org/widgets/simplex-chat/cs/website/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/website/cs/)|[](https://github.com/simplex-chat/simplex-chat/tree/master/docs/lang/cs)|
|🇩🇪 de|Deutsch |[mlanp](https://github.com/mlanp)|[![android app](https://hosted.weblate.org/widgets/simplex-chat/de/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/de/)<br>[![ios app](https://hosted.weblate.org/widgets/simplex-chat/de/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/de/)|[![website](https://hosted.weblate.org/widgets/simplex-chat/de/website/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/website/de/)||
|🇪🇸 es|Español |[Mateyhv](https://github.com/Mateyhv)|[![android app](https://hosted.weblate.org/widgets/simplex-chat/es/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/es/)<br>[![ios app](https://hosted.weblate.org/widgets/simplex-chat/es/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/es/)|[![website](https://hosted.weblate.org/widgets/simplex-chat/es/website/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/website/es/)||
|🇫🇷 fr|Français |[ishi_sama](https://github.com/ishi-sama)|[![android app](https://hosted.weblate.org/widgets/simplex-chat/fr/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/fr/)<br>[![ios app](https://hosted.weblate.org/widgets/simplex-chat/fr/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/fr/)|[![website](https://hosted.weblate.org/widgets/simplex-chat/fr/website/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/website/fr/)|[](https://github.com/simplex-chat/simplex-chat/tree/master/docs/lang/fr)|
|🇮🇹 it|Italiano |[unbranched](https://github.com/unbranched)|[![android app](https://hosted.weblate.org/widgets/simplex-chat/it/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/it/)<br>[![ios app](https://hosted.weblate.org/widgets/simplex-chat/it/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/it/)|[![website](https://hosted.weblate.org/widgets/simplex-chat/it/website/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/website/it/)||
|🇳🇱 nl|Nederlands|[mika-nl](https://github.com/mika-nl)|[![android app](https://hosted.weblate.org/widgets/simplex-chat/nl/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/nl/)<br>[![ios app](https://hosted.weblate.org/widgets/simplex-chat/nl/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/nl/)|[![website](https://hosted.weblate.org/widgets/simplex-chat/nl/website/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/website/nl/)||
|🇵🇱 pl|Polski |[BxOxSxS](https://github.com/BxOxSxS)|[![android app](https://hosted.weblate.org/widgets/simplex-chat/pl/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/ru/)<br>[![ios app](https://hosted.weblate.org/widgets/simplex-chat/ru/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/pl/)|||
|🇷🇺 ru|Русский ||[![android app](https://hosted.weblate.org/widgets/simplex-chat/ru/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/ru/)<br>[![ios app](https://hosted.weblate.org/widgets/simplex-chat/ru/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/ru/)|||
|🇨🇳 zh-CHS|简体中文|[sith-on-mars](https://github.com/sith-on-mars)<br><br>[Float-hu](https://github.com/Float-hu)|[![android app](https://hosted.weblate.org/widgets/simplex-chat/zh_Hans/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/zh_Hans/)<br>[![ios app](https://hosted.weblate.org/widgets/simplex-chat/zh_Hans/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/zh_Hans/)<br>&nbsp;|<br><br>[![website](https://hosted.weblate.org/widgets/simplex-chat/zh_Hans/website/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/website/zh_Hans/)||
Languages in progress: Arabic, Japanese, Korean, Portuguese and [others](https://hosted.weblate.org/projects/simplex-chat/#languages). We will be adding more languages as some of the already added are completed please suggest new languages, review the [translation guide](./docs/TRANSLATIONS.md) and get in touch with us!
## Contribute
We would love to have you join the development! You can help us with:
- writing a tutorial or recipes about hosting servers, chat bot automations, etc.
- contributing to SimpleX Chat knowledge-base.
- developing features - please connect to us via chat so we can help you get started.
## Help us with donations
Huge thank you to everybody who donated to SimpleX Chat!
We are prioritizing users privacy and security - it would be impossible without your support.
Our pledge to our users is that SimpleX protocols are and will remain open, and in public domain, - so anybody can build the future implementations of the clients and the servers. We are building SimpleX platform based on the same principles as email and web, but much more private and secure.
Your donations help us raise more funds any amount, even the price of the cup of coffee, would make a big difference for us.
It is possible to donate via:
- [GitHub](https://github.com/sponsors/simplex-chat) - it is commission-free for us.
- [OpenCollective](https://opencollective.com/simplex-chat) - it charges a commission, and also accepts donations in crypto-currencies.
- Monero address: 8568eeVjaJ1RQ65ZUn9PRQ8ENtqeX9VVhcCYYhnVLxhV4JtBqw42so2VEUDQZNkFfsH5sXCuV7FN8VhRQ21DkNibTZP57Qt
- Bitcoin address: 1bpefFkzuRoMY3ZuBbZNZxycbg7NYPYTG
- BCH address: 1bpefFkzuRoMY3ZuBbZNZxycbg7NYPYTG
- Ethereum address: 0x83fd788f7241a2be61780ea9dc72d2151e6843e2
- Solana address: 43tWFWDczgAcn4Rzwkpqg2mqwnQETSiTwznmCgA2tf1L
Thank you,
Evgeny
SimpleX Chat founder
## Contents ## Contents
@@ -37,15 +152,11 @@
- [Users own SimpleX network](#users-own-simplex-network) - [Users own SimpleX network](#users-own-simplex-network)
- [Frequently asked questions](#frequently-asked-questions) - [Frequently asked questions](#frequently-asked-questions)
- [News and updates](#news-and-updates) - [News and updates](#news-and-updates)
- [Make a private connection](#make-a-private-connection)
- [Quick installation of a terminal app](#zap-quick-installation-of-a-terminal-app) - [Quick installation of a terminal app](#zap-quick-installation-of-a-terminal-app)
- [SimpleX Platform design](#simplex-platform-design) - [SimpleX Platform design](#simplex-platform-design)
- [Privacy: technical details and limitations](#privacy-technical-details-and-limitations) - [Privacy: technical details and limitations](#privacy-technical-details-and-limitations)
- [For developers](#for-developers) - [For developers](#for-developers)
- [Roadmap](#roadmap) - [Roadmap](#roadmap)
- [Join a user group](#join-a-user-group)
- [Contribute](#contribute)
- [Help us with donations](#help-us-with-donations)
- [Disclaimers, Security contact, License](#disclaimers) - [Disclaimers, Security contact, License](#disclaimers)
## Why privacy matters ## Why privacy matters
@@ -76,36 +187,34 @@ You can use SimpleX with your own servers and still communicate with people usin
## Frequently asked questions ## Frequently asked questions
1. _How SimpleX can deliver messages without any user identifiers?_ See [v2 release annoucement](./blog/20220511-simplex-chat-v2-images-files.md#the-first-messaging-platform-without-user-identifiers) explaining how SimpleX works. 1. _How SimpleX can deliver messages without any user identifiers?_ See [v2 release announcement](./blog/20220511-simplex-chat-v2-images-files.md#the-first-messaging-platform-without-user-identifiers) explaining how SimpleX works.
2. _Why should I not just use Signal?_ Signal is a centralised platform that uses phone numbers to identify its users and their contacts. It means that while the content of your messages on Signal is protected with robust end-to-end encryption, there is a large amount of meta-data visible to Signal - who you talk with and when. 2. _Why should I not just use Signal?_ Signal is a centralized platform that uses phone numbers to identify its users and their contacts. It means that while the content of your messages on Signal is protected with robust end-to-end encryption, there is a large amount of meta-data visible to Signal - who you talk with and when.
3. _How is it different from Matrix, Session, Ricochet, Cwtch, etc., that also don't require user identites?_ Although these platforms do not require a _real identity_, they do rely on anonymous user identities to deliver messages it can be, for example, an identity key or a random number. Using a persistent user identity, even anonymous, creates a risk that user's connection graph becomes known to the observers and/or service providers, and it can lead to de-anonymizing some users. If the same user profile is used to connect to two different people via any messenger other than SimpleX, these two people can confirm if they are connected to the same person - they would use the same user identifier in the messages. With SimpleX there is no meta-data in common between your conversations with different contacts - the quality that no other messaging platform has. 3. _How is it different from Matrix, Session, Ricochet, Cwtch, etc., that also don't require user identities?_ Although these platforms do not require a _real identity_, they do rely on anonymous user identities to deliver messages it can be, for example, an identity key or a random number. Using a persistent user identity, even anonymous, creates a risk that user's connection graph becomes known to the observers and/or service providers, and it can lead to de-anonymizing some users. If the same user profile is used to connect to two different people via any messenger other than SimpleX, these two people can confirm if they are connected to the same person - they would use the same user identifier in the messages. With SimpleX there is no meta-data in common between your conversations with different contacts - the quality that no other messaging platform has.
## News and updates ## News and updates
Recent updates: Recent updates:
[Jan 03, 2023. v4.4 released - with disappearing messages, "live" messages, connection security verifications, GIFs and stickers and with French interface language](./blog/20230103-simplex-chat-v4.4-disappearing-messages.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).
[Dec 06, 2022. November reviews and v4.3 released - with instant voice messages, irreversible deletion of sent messages and improved server configuration](./blog/20221206-simplex-chat-v4.3-voice-messages.md). [Mar 28, 2023. v4.6 released - with Android 8+ and ARMv7a support, hidden profiles, community moderation, improved audio/video calls and reduced battery usage](./blog/20230328-simplex-chat-v4-6-hidden-profiles.md).
[Nov 08, 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). [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).
[Feb 4, 2023. v4.5 released - with multiple user profiles, message draft, transport isolation and Italian interface](./blog/20230204-simplex-chat-v4-5-user-chat-profiles.md).
[Jan 3, 2023. v4.4 released - with disappearing messages, "live" messages, connection security verifications, GIFs and stickers and with French interface language](./blog/20230103-simplex-chat-v4.4-disappearing-messages.md).
[Dec 6, 2022. November reviews and v4.3 released - with instant voice messages, irreversible deletion of sent messages and improved server configuration](./blog/20221206-simplex-chat-v4.3-voice-messages.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). [Sep 28, 2022. v4.0: encrypted local chat database and many other changes](./blog/20220928-simplex-chat-v4-encrypted-database.md).
[Sep 1, 2022. v3.2: incognito mode, support .onion server hostnames, setting contact names, changing color scheme, etc. Implementation audit is arranged for October!](./blog/20220901-simplex-chat-v3.2-incognito-mode.md).
[All updates](./blog) [All updates](./blog)
## Make a private connection
You need to share a link or scan a QR code (in person or during a video call) to make a connection and start messaging.
The channel through which you share the link does not have to be secure - it is enough that you can confirm who sent you the message and that your SimpleX connection is established.
<img src="https://github.com/simplex-chat/.github/blob/master/profile/images/app1.png" alt="Make a private connection" height="360"> <img src="https://github.com/simplex-chat/.github/blob/master/profile/images/arrow.png" height="360"> <img src="https://github.com/simplex-chat/.github/blob/master/profile/images/app2.png" alt="Conversation" height="360"> <img src="https://github.com/simplex-chat/.github/blob/master/profile/images/arrow.png" height="360"> <img src="https://github.com/simplex-chat/.github/blob/master/profile/images/app3.png" alt="Video call" height="360">
## :zap: Quick installation of a terminal app ## :zap: Quick installation of a terminal app
```sh ```sh
@@ -138,7 +247,7 @@ SimpleX Chat is a work in progress we are releasing improvements as they are
What is already implemented: What is already implemented:
1. Instead of user profile identifiers used by all other platforms, even the most private ones, SimpleX uses pairwise per-queue identifiers (2 addresses for each unidirectional message queue, with an optional 3rd address for push notificaitons on iOS, 2 queues in each connection between the users). It makes observing the network graph on the application level more difficult, as for `n` users there can be up to `n * (n-1)` message queues. 1. Instead of user profile identifiers used by all other platforms, even the most private ones, SimpleX uses pairwise per-queue identifiers (2 addresses for each unidirectional message queue, with an optional 3rd address for push notifications on iOS, 2 queues in each connection between the users). It makes observing the network graph on the application level more difficult, as for `n` users there can be up to `n * (n-1)` message queues.
2. End-to-end encryption in each message queue using [NaCl cryptobox](https://nacl.cr.yp.to/box.html). This is added to allow redundancy in the future (passing each message via several servers), to avoid having the same ciphertext in different queues (that would only be visible to the attacker if TLS is compromised). The encryption keys used for this encryption are not rotated, instead we are planning to rotate the queues. Curve25519 keys are used for key negotiation. 2. End-to-end encryption in each message queue using [NaCl cryptobox](https://nacl.cr.yp.to/box.html). This is added to allow redundancy in the future (passing each message via several servers), to avoid having the same ciphertext in different queues (that would only be visible to the attacker if TLS is compromised). The encryption keys used for this encryption are not rotated, instead we are planning to rotate the queues. Curve25519 keys are used for key negotiation.
3. [Double ratchet](https://signal.org/docs/specifications/doubleratchet/) end-to-end encryption in each conversation between two users (or group members). This is the same algorithm that is used in Signal and many other messaging apps; it provides OTR messaging with forward secrecy (each message is encrypted by its own ephemeral key), break-in recovery (the keys are frequently re-negotiated as part of the message exchange). Two pairs of Curve448 keys are used for the initial key agreement, initiating party passes these keys via the connection link, accepting side - in the header of the confirmation message. 3. [Double ratchet](https://signal.org/docs/specifications/doubleratchet/) end-to-end encryption in each conversation between two users (or group members). This is the same algorithm that is used in Signal and many other messaging apps; it provides OTR messaging with forward secrecy (each message is encrypted by its own ephemeral key), break-in recovery (the keys are frequently re-negotiated as part of the message exchange). Two pairs of Curve448 keys are used for the initial key agreement, initiating party passes these keys via the connection link, accepting side - in the header of the confirmation message.
4. Additional layer of encryption using NaCL cryptobox for the messages delivered from the server to the recipient. This layer avoids having any ciphertext in common between sent and received traffic of the server inside TLS (and there are no identifiers in common as well). 4. Additional layer of encryption using NaCL cryptobox for the messages delivered from the server to the recipient. This layer avoids having any ciphertext in common between sent and received traffic of the server inside TLS (and there are no identifiers in common as well).
@@ -148,10 +257,11 @@ What is already implemented:
8. To protect against replay attacks SimpleX servers require [tlsunique channel binding](https://www.rfc-editor.org/rfc/rfc5929.html) as session ID in each client command signed with per-queue ephemeral key. 8. To protect against replay attacks SimpleX servers require [tlsunique channel binding](https://www.rfc-editor.org/rfc/rfc5929.html) as session ID in each client command signed with per-queue ephemeral key.
9. To protect your IP address all SimpleX Chat clients support accessing messaging servers via Tor - see [v3.1 release announcement](./blog/20220808-simplex-chat-v3.1-chat-groups.md) for more details. 9. To protect your IP address all SimpleX Chat clients support accessing messaging servers via Tor - see [v3.1 release announcement](./blog/20220808-simplex-chat-v3.1-chat-groups.md) for more details.
10. Local database encryption with passphrase - your contacts, groups and all sent and received messages are stored encrypted. If you used SimpleX Chat before v4.0 you need to enable the encryption via the app settings. 10. Local database encryption with passphrase - your contacts, groups and all sent and received messages are stored encrypted. If you used SimpleX Chat before v4.0 you need to enable the encryption via the app settings.
11. Transport isolation - different TCP connections and Tor circuits are used for traffic of different user profiles, optionally - for different contacts and group member connections.
We plan to add soon: We plan to add soon:
1. Message queue rotation. Currently the queues created between two users are used until the contact is deleted, providing a long-term pairwise identifiers of the conversation. We are planning to add queue rotation to make these identifiers temporary and rotate based on some schedule TBC (e.g., every X messages, or every X hours/days). 1. Automatic message queue rotation. Currently the queues created between two users are used until the queue is manually changed by the user or contact is deleted. We are planning to add automatic queue rotation to make these identifiers temporary and rotate based on some schedule TBC (e.g., every X messages, or every X hours/days).
2. Local files encryption. Currently the images and files you send and receive are stored in the app unencrypted, you can delete them via `Settings / Database passphrase & export`. 2. Local files encryption. Currently the images and files you send and receive are stored in the app unencrypted, you can delete them via `Settings / Database passphrase & export`.
3. Message "mixing" - adding latency to message delivery, to protect against traffic correlation by message time. 3. Message "mixing" - adding latency to message delivery, to protect against traffic correlation by message time.
@@ -161,7 +271,7 @@ You can:
- use SimpleX Chat library to integrate chat functionality into your mobile apps. - use SimpleX Chat library to integrate chat functionality into your mobile apps.
- create chat bots and services in Haskell - see [simple](./apps/simplex-bot/) and more [advanced chat bot example](./apps/simplex-bot-advanced/). - create chat bots and services in Haskell - see [simple](./apps/simplex-bot/) and more [advanced chat bot example](./apps/simplex-bot-advanced/).
- create chat bots and services in any language running SimpleX Chat terminal CLI as a local WebSocket server. See [TypeScript SimpleX Chat client](./packages/simplex-chat-client/) and [JavaScipt chat bot example](./packages/simplex-chat-client/typescript/examples/squaring-bot.js). - create chat bots and services in any language running SimpleX Chat terminal CLI as a local WebSocket server. See [TypeScript SimpleX Chat client](./packages/simplex-chat-client/) and [JavaScript chat bot example](./packages/simplex-chat-client/typescript/examples/squaring-bot.js).
- run [simplex-chat terminal CLI](./docs/CLI.md) to execute individual chat commands, e.g. to send messages as part of shell script execution. - run [simplex-chat terminal CLI](./docs/CLI.md) to execute individual chat commands, e.g. to send messages as part of shell script execution.
If you are considering developing with SimpleX platform please get in touch for any advice and support. If you are considering developing with SimpleX platform please get in touch for any advice and support.
@@ -196,17 +306,26 @@ If you are considering developing with SimpleX platform please get in touch for
- ✅ Disappearing messages (with recipient opt-in per-contact). - ✅ Disappearing messages (with recipient opt-in per-contact).
- ✅ "Live" messages. - ✅ "Live" messages.
- ✅ Contact verification via a separate out-of-band channel. - ✅ Contact verification via a separate out-of-band channel.
- 🏗 Multiple user profiles in the same chat database. - Multiple user profiles in the same chat database.
- 🏗 Optionally avoid re-using the same TCP session for multiple connections. - Optionally avoid re-using the same TCP session for multiple connections.
- 🏗 File server to optimize for efficient and private sending of large files. - ✅ Preserve message drafts.
- ✅ File server to optimize for efficient and private sending of large files.
- ✅ Improved audio & video calls.
- ✅ Support older Android OS and 32-bit CPUs.
- ✅ Hidden chat profiles.
- ✅ Sending and receiving large files via [XFTP protocol](./blog/20230301-simplex-file-transfer-protocol.md).
- ✅ Video messages.
- ✅ App access passcode.
- 🏗 Improved Android app UI design.
- 🏗 SMP queue redundancy and rotation (manual is supported). - 🏗 SMP queue redundancy and rotation (manual is supported).
- 🏗 Reduced battery and traffic usage in large groups. - 🏗 Reduced battery and traffic usage in large groups.
- 🏗 Preserve message drafts. - Include optional message into connection request sent via contact address.
- 🏗 Support older Android OS and 32-bit CPUs.
- Ephemeral/disappearing/OTR conversations with the existing contacts. - Ephemeral/disappearing/OTR conversations with the existing contacts.
- Access password/pin (with optional alternative access password). - Optional alternative access password.
- Video messages. - Local app files encryption.
- Improved navigation and search in the conversation (expand and scroll to quoted message, scroll to search results, etc.).
- Message delivery confirmation (with sender opt-in or opt-out per contact, TBC). - Message delivery confirmation (with sender opt-in or opt-out per contact, TBC).
- Privately share your location.
- Feeds/broadcasts. - Feeds/broadcasts.
- Web widgets for custom interactivity in the chats. - Web widgets for custom interactivity in the chats.
- Programmable chat automations / rules (automatic replies/forward/deletion/sending, reminders, etc.). - Programmable chat automations / rules (automatic replies/forward/deletion/sending, reminders, etc.).
@@ -215,53 +334,9 @@ If you are considering developing with SimpleX platform please get in touch for
- Privacy-preserving identity server for optional DNS-based contact/group addresses to simplify connection and discovery, but not used to deliver messages: - Privacy-preserving identity server for optional DNS-based contact/group addresses to simplify connection and discovery, but not used to deliver messages:
- keep all your contacts and groups even if you lose the domain. - keep all your contacts and groups even if you lose the domain.
- the server doesn't have information about your contacts and groups. - the server doesn't have information about your contacts and groups.
- Channels server for large groups and broadcast channels. - Hosting server for large groups, communities and public channels.
- Message delivery relay for senders (to conceal IP address from the recipients' servers and to reduce the traffic).
## Join a user group - High capacity multi-node SMP relays.
You can join a general English speaking group: [#SimpleX-Group](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2Fhpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg%3D%40smp5.simplex.im%2FcIS0gu1h0Y8pZpQkDaSz7HZGSHcKpMB9%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAKzzWAJYrVt1zdgRp4pD3FBst6eK7233DJeNElENLJRA%253D%26srv%3Djjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%228mazMhefXoM5HxWBfZnvwQ%3D%3D%22%7D).
Groups in languages other than English, that we have app interface translated into: [\#SimpleX-DE](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FkIEl7OQzcp-J6aDmjdlQbRJwqkcZE7XR%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAR16PCu02MobRmKAsjzhDWMZcWP9hS8l5AUZi-Gs8z18%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22puYPMCQt11yPUvgmI5jCiw%3D%3D%22%7D) (German speaking), [\#SimpleX-FR](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2Fhpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg%3D%40smp5.simplex.im%2FvIHQDxTor53nwnWWTy5cHNwQQAdWN5Hw%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAPdgK1eBnETmgiqEQufbUkydKBJafoRx4iRrtrC2NAGc%253D%26srv%3Djjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%221FyUryBPza-1ZFFE80Ekbg%3D%3D%22%7D) (French speaking), [\#SimpleX-RU](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FXZyt3hJmWsycpN7Dqve_wbrAqb6myk1R%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAMFVIoytozTEa_QXOgoZFq_oe0IwZBYKvW50trSFXzXo%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22xz05ngjA3pNIxLZ32a8Vxg%3D%3D%22%7D) (Russian speaking).
You can join these groups either by opening these links in the app or by opening them in a desktop browser and scanning QR code.
Join via the app to share what's going on and ask any questions!
## Contribute
We would love to have you join the development! You can contribute to SimpleX Chat with:
- translate UI to your language - we are using [Weblate](https://hosted.weblate.org/projects/simplex-chat/) to translate the interface, please get in touch if you want to contribute!
- translate website homepage - there is a lot of content we would like to share, it would help to bring the new users.
- writing a tutorial or recipes about hosting servers, chat bot automations, etc.
- developing features - please connect to us via chat so we can help you get started.
## Help us with donations
Huge thank you to everybody who donated to SimpleX Chat!
We are prioritizing users privacy and security - it would be impossible without your support.
Our pledge to our users is that SimpleX protocols are and will remain open, and in public domain, - so anybody can build the future implementations of the clients and the servers. We are building SimpleX platform based on the same principles as email and web, but much more private and secure.
Your donations help us raise more funds any amount, even the price of the cup of coffee, would make a big difference for us.
It is possible to donate via:
- [GitHub](https://github.com/sponsors/simplex-chat) - it is commission-free for us.
- [OpenCollective](https://opencollective.com/simplex-chat) - it charges a commission, and also accepts donations in crypto-currencies.
- Monero address: 8568eeVjaJ1RQ65ZUn9PRQ8ENtqeX9VVhcCYYhnVLxhV4JtBqw42so2VEUDQZNkFfsH5sXCuV7FN8VhRQ21DkNibTZP57Qt
- Bitcoin address: 1bpefFkzuRoMY3ZuBbZNZxycbg7NYPYTG
- BCH address: 1bpefFkzuRoMY3ZuBbZNZxycbg7NYPYTG
- Ethereum address: 0x83fd788f7241a2be61780ea9dc72d2151e6843e2
- Solana address: 43tWFWDczgAcn4Rzwkpqg2mqwnQETSiTwznmCgA2tf1L
- please let us know, via GitHub issue or chat, if you want to create a donation in some other cryptocurrency - we will add the address to the list.
Thank you,
Evgeny
SimpleX Chat founder
## Disclaimers ## Disclaimers
+50 -14
View File
@@ -9,15 +9,12 @@ android {
defaultConfig { defaultConfig {
applicationId "chat.simplex.app" applicationId "chat.simplex.app"
minSdk 29 minSdk 26
targetSdk 32 targetSdk 32
versionCode 90 versionCode 117
versionName "4.4.4-beta.1" versionName "5.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
ndk {
abiFilters 'arm64-v8a'
}
vectorDrawables { vectorDrawables {
useSupportLibrary true useSupportLibrary true
} }
@@ -76,6 +73,39 @@ android {
} }
jniLibs.useLegacyPackaging = compression_level != "0" jniLibs.useLegacyPackaging = compression_level != "0"
} }
def isRelease = gradle.getStartParameter().taskNames.find({ it.toLowerCase().contains("release") }) != null
def isBundle = gradle.getStartParameter().taskNames.find({ it.toLowerCase().contains("bundle") }) != null
// if (isRelease) {
// Comma separated list of languages that will be included in the apk
android.defaultConfig.resConfigs(
"en",
"cs",
"de",
"es",
"fr",
"it",
"nl",
"pl",
"ru",
"zh-rCN"
)
// }
if (isBundle) {
defaultConfig.ndk.abiFilters 'arm64-v8a', 'armeabi-v7a'
} else {
splits {
abi {
enable true
reset()
if (isRelease) {
include 'arm64-v8a', 'armeabi-v7a'
} else {
include 'arm64-v8a', 'armeabi-v7a'
universalApk false
}
}
}
}
} }
dependencies { dependencies {
@@ -89,7 +119,8 @@ dependencies {
implementation 'androidx.fragment:fragment:1.4.1' implementation 'androidx.fragment:fragment:1.4.1'
implementation 'org.jetbrains.kotlinx:kotlinx-datetime:0.3.2' implementation 'org.jetbrains.kotlinx:kotlinx-datetime:0.3.2'
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.2' implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.2'
implementation "androidx.compose.material:material-icons-extended:$compose_version" implementation 'com.charleskorn.kaml:kaml:0.43.0'
//implementation "androidx.compose.material:material-icons-extended:$compose_version"
implementation "androidx.compose.ui:ui-util:$compose_version" implementation "androidx.compose.ui:ui-util:$compose_version"
implementation "androidx.navigation:navigation-compose:2.4.1" implementation "androidx.navigation:navigation-compose:2.4.1"
implementation "com.google.accompanist:accompanist-insets:0.23.0" implementation "com.google.accompanist:accompanist-insets:0.23.0"
@@ -124,6 +155,9 @@ dependencies {
implementation "io.coil-kt:coil-compose:2.1.0" implementation "io.coil-kt:coil-compose:2.1.0"
implementation "io.coil-kt:coil-gif:2.1.0" implementation "io.coil-kt:coil-gif:2.1.0"
// Video support
implementation "com.google.android.exoplayer:exoplayer:2.17.1"
testImplementation 'junit:junit:4.13.2' testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
@@ -131,19 +165,12 @@ dependencies {
debugImplementation "androidx.compose.ui:ui-tooling:$compose_version" debugImplementation "androidx.compose.ui:ui-tooling:$compose_version"
} }
def buildType = "unknown"
// Don't do anything if no compression is needed // Don't do anything if no compression is needed
if (compression_level != "0") { if (compression_level != "0") {
tasks.whenTaskAdded { task -> tasks.whenTaskAdded { task ->
if (task.name == 'packageDebug') { if (task.name == 'packageDebug') {
task.doLast {
buildType = "debug"
}
task.finalizedBy compressApk task.finalizedBy compressApk
} else if (task.name == 'packageRelease') { } else if (task.name == 'packageRelease') {
task.doLast {
buildType = "release"
}
task.finalizedBy compressApk task.finalizedBy compressApk
} }
} }
@@ -151,6 +178,13 @@ if (compression_level != "0") {
tasks.register("compressApk") { tasks.register("compressApk") {
doLast { doLast {
def isRelease = gradle.getStartParameter().taskNames.find({ it.toLowerCase().contains("release") }) != null
def buildType
if (isRelease) {
buildType = "release"
} else {
buildType = "debug"
}
def javaHome = System.properties['java.home'] ?: org.gradle.internal.jvm.Jvm.current().getJavaHome() def javaHome = System.properties['java.home'] ?: org.gradle.internal.jvm.Jvm.current().getJavaHome()
def sdkDir = android.getSdkDirectory().getAbsolutePath() def sdkDir = android.getSdkDirectory().getAbsolutePath()
def keyAlias = "" def keyAlias = ""
@@ -191,6 +225,8 @@ tasks.register("compressApk") {
if (project.properties['android.injected.signing.key.alias'] != null && buildType == 'release') { if (project.properties['android.injected.signing.key.alias'] != null && buildType == 'release') {
new File(outputDir, "app-release.apk").renameTo(new File(outputDir, "simplex.apk")) new File(outputDir, "app-release.apk").renameTo(new File(outputDir, "simplex.apk"))
new File(outputDir, "app-armeabi-v7a-release.apk").renameTo(new File(outputDir, "simplex-armv7a.apk"))
new File(outputDir, "app-arm64-v8a-release.apk").renameTo(new File(outputDir, "simplex.apk"))
} }
// View all gradle properties set // View all gradle properties set
@@ -31,6 +31,7 @@
android:extractNativeLibs="${extract_native_libs}" android:extractNativeLibs="${extract_native_libs}"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/Theme.SimpleX"> android:theme="@style/Theme.SimpleX">
<!-- android:localeConfig="@xml/locales_config"-->
<!-- Main activity --> <!-- Main activity -->
<activity <activity
+18 -1
View File
@@ -22,10 +22,12 @@ var TransformOperation;
TransformOperation["Decrypt"] = "decrypt"; TransformOperation["Decrypt"] = "decrypt";
})(TransformOperation || (TransformOperation = {})); })(TransformOperation || (TransformOperation = {}));
let activeCall; let activeCall;
let answerTimeout = 30000;
const processCommand = (function () { const processCommand = (function () {
const defaultIceServers = [ const defaultIceServers = [
{ urls: ["stun:stun.simplex.im:443"] }, { urls: ["stun:stun.simplex.im:443"] },
{ urls: ["turn:turn.simplex.im:443"], username: "private", credential: "yleob6AVkiNI87hpR94Z" }, { urls: ["turn:turn.simplex.im:443?transport=udp"], username: "private", credential: "yleob6AVkiNI87hpR94Z" },
{ urls: ["turn:turn.simplex.im:443?transport=tcp"], username: "private", credential: "yleob6AVkiNI87hpR94Z" },
]; ];
function getCallConfig(encodedInsertableStreams, iceServers, relay) { function getCallConfig(encodedInsertableStreams, iceServers, relay) {
return { return {
@@ -100,9 +102,16 @@ const processCommand = (function () {
const iceCandidates = getIceCandidates(pc, config); const iceCandidates = getIceCandidates(pc, config);
const call = { connection: pc, iceCandidates, localMedia: mediaType, localCamera, localStream, remoteStream, aesKey, useWorker }; const call = { connection: pc, iceCandidates, localMedia: mediaType, localCamera, localStream, remoteStream, aesKey, useWorker };
await setupMediaStreams(call); await setupMediaStreams(call);
let connectionTimeout = setTimeout(connectionHandler, answerTimeout);
pc.addEventListener("connectionstatechange", connectionStateChange); pc.addEventListener("connectionstatechange", connectionStateChange);
return call; return call;
async function connectionStateChange() { async function connectionStateChange() {
// "failed" means the second party did not answer in time (15 sec timeout in Chrome WebView)
// See https://source.chromium.org/chromium/chromium/src/+/main:third_party/webrtc/p2p/base/p2p_constants.cc;l=70)
if (pc.connectionState !== "failed")
connectionHandler();
}
async function connectionHandler() {
sendMessageToNative({ sendMessageToNative({
resp: { resp: {
type: "connection", type: "connection",
@@ -115,6 +124,7 @@ const processCommand = (function () {
}, },
}); });
if (pc.connectionState == "disconnected" || pc.connectionState == "failed") { if (pc.connectionState == "disconnected" || pc.connectionState == "failed") {
clearConnectionTimeout();
pc.removeEventListener("connectionstatechange", connectionStateChange); pc.removeEventListener("connectionstatechange", connectionStateChange);
if (activeCall) { if (activeCall) {
setTimeout(() => sendMessageToNative({ resp: { type: "ended" } }), 0); setTimeout(() => sendMessageToNative({ resp: { type: "ended" } }), 0);
@@ -122,6 +132,7 @@ const processCommand = (function () {
endCall(); endCall();
} }
else if (pc.connectionState == "connected") { else if (pc.connectionState == "connected") {
clearConnectionTimeout();
const stats = (await pc.getStats()); const stats = (await pc.getStats());
for (const stat of stats.values()) { for (const stat of stats.values()) {
const { type, state } = stat; const { type, state } = stat;
@@ -141,6 +152,12 @@ const processCommand = (function () {
} }
} }
} }
function clearConnectionTimeout() {
if (connectionTimeout) {
clearTimeout(connectionTimeout);
connectionTimeout = undefined;
}
}
} }
function serialize(x) { function serialize(x) {
return LZString.compressToBase64(JSON.stringify(x)); return LZString.compressToBase64(JSON.stringify(x));
+1 -5
View File
@@ -53,10 +53,6 @@ add_library( support SHARED IMPORTED )
set_target_properties( support PROPERTIES IMPORTED_LOCATION set_target_properties( support PROPERTIES IMPORTED_LOCATION
${CMAKE_SOURCE_DIR}/libs/${ANDROID_ABI}/libsupport.so) ${CMAKE_SOURCE_DIR}/libs/${ANDROID_ABI}/libsupport.so)
add_library( crypto SHARED IMPORTED )
set_target_properties( crypto PROPERTIES IMPORTED_LOCATION
${CMAKE_SOURCE_DIR}/libs/${ANDROID_ABI}/libcrypto.so)
# Specifies libraries CMake should link to your target library. You # Specifies libraries CMake should link to your target library. You
# can link multiple libraries, such as libraries you define in this # can link multiple libraries, such as libraries you define in this
# build script, prebuilt third-party libraries, or system libraries. # build script, prebuilt third-party libraries, or system libraries.
@@ -64,7 +60,7 @@ set_target_properties( crypto PROPERTIES IMPORTED_LOCATION
target_link_libraries( # Specifies the target library. target_link_libraries( # Specifies the target library.
app-lib app-lib
simplex support crypto simplex support
# Links the target library to the log library # Links the target library to the log library
# included in the NDK. # included in the NDK.
+27 -3
View File
@@ -7,6 +7,17 @@ void hs_init(int * argc, char **argv[]);
void setLineBuffering(void); void setLineBuffering(void);
int pipe_std_to_socket(const char * name); int pipe_std_to_socket(const char * name);
extern void __svfscanf(void){};
extern void __vfwscanf(void){};
extern void __memset_chk_fail(void){};
extern void __strcpy_chk_generic(void){};
extern void __strcat_chk_generic(void){};
extern void __libc_globals(void){};
extern void __rel_iplt_start(void){};
// Android 9 only, not 13
extern void reallocarray(void){};
JNIEXPORT jint JNICALL JNIEXPORT jint JNICALL
Java_chat_simplex_app_SimplexAppKt_pipeStdOutToSocket(JNIEnv *env, __unused jclass clazz, jstring socket_name) { Java_chat_simplex_app_SimplexAppKt_pipeStdOutToSocket(JNIEnv *env, __unused jclass clazz, jstring socket_name) {
const char *name = (*env)->GetStringUTFChars(env, socket_name, JNI_FALSE); const char *name = (*env)->GetStringUTFChars(env, socket_name, JNI_FALSE);
@@ -24,21 +35,24 @@ Java_chat_simplex_app_SimplexAppKt_initHS(__unused JNIEnv *env, __unused jclass
// from simplex-chat // from simplex-chat
typedef long* chat_ctrl; typedef long* chat_ctrl;
extern char *chat_migrate_init(const char *path, const char *key, chat_ctrl *ctrl); extern char *chat_migrate_init(const char *path, const char *key, const char *confirm, chat_ctrl *ctrl);
extern char *chat_send_cmd(chat_ctrl ctrl, const char *cmd); extern char *chat_send_cmd(chat_ctrl ctrl, const char *cmd);
extern char *chat_recv_msg(chat_ctrl ctrl); // deprecated extern char *chat_recv_msg(chat_ctrl ctrl); // deprecated
extern char *chat_recv_msg_wait(chat_ctrl ctrl, const int wait); extern char *chat_recv_msg_wait(chat_ctrl ctrl, const int wait);
extern char *chat_parse_markdown(const char *str); extern char *chat_parse_markdown(const char *str);
extern char *chat_parse_server(const char *str); extern char *chat_parse_server(const char *str);
extern char *chat_password_hash(const char *pwd, const char *salt);
JNIEXPORT jobjectArray JNICALL JNIEXPORT jobjectArray JNICALL
Java_chat_simplex_app_SimplexAppKt_chatMigrateInit(JNIEnv *env, __unused jclass clazz, jstring dbPath, jstring dbKey) { Java_chat_simplex_app_SimplexAppKt_chatMigrateInit(JNIEnv *env, __unused jclass clazz, jstring dbPath, jstring dbKey, jstring confirm) {
const char *_dbPath = (*env)->GetStringUTFChars(env, dbPath, JNI_FALSE); const char *_dbPath = (*env)->GetStringUTFChars(env, dbPath, JNI_FALSE);
const char *_dbKey = (*env)->GetStringUTFChars(env, dbKey, JNI_FALSE); const char *_dbKey = (*env)->GetStringUTFChars(env, dbKey, JNI_FALSE);
const char *_confirm = (*env)->GetStringUTFChars(env, confirm, JNI_FALSE);
jlong _ctrl = (jlong) 0; jlong _ctrl = (jlong) 0;
jstring res = (*env)->NewStringUTF(env, chat_migrate_init(_dbPath, _dbKey, &_ctrl)); jstring res = (*env)->NewStringUTF(env, chat_migrate_init(_dbPath, _dbKey, _confirm, &_ctrl));
(*env)->ReleaseStringUTFChars(env, dbPath, _dbPath); (*env)->ReleaseStringUTFChars(env, dbPath, _dbPath);
(*env)->ReleaseStringUTFChars(env, dbKey, _dbKey); (*env)->ReleaseStringUTFChars(env, dbKey, _dbKey);
(*env)->ReleaseStringUTFChars(env, dbKey, _confirm);
// Creating array of Object's (boxed values can be passed, eg. Long instead of long) // Creating array of Object's (boxed values can be passed, eg. Long instead of long)
jobjectArray ret = (jobjectArray)(*env)->NewObjectArray(env, 2, (*env)->FindClass(env, "java/lang/Object"), NULL); jobjectArray ret = (jobjectArray)(*env)->NewObjectArray(env, 2, (*env)->FindClass(env, "java/lang/Object"), NULL);
@@ -85,3 +99,13 @@ Java_chat_simplex_app_SimplexAppKt_chatParseServer(JNIEnv *env, __unused jclass
(*env)->ReleaseStringUTFChars(env, str, _str); (*env)->ReleaseStringUTFChars(env, str, _str);
return res; return res;
} }
JNIEXPORT jstring JNICALL
Java_chat_simplex_app_SimplexAppKt_chatPasswordHash(JNIEnv *env, __unused jclass clazz, jstring pwd, jstring salt) {
const char *_pwd = (*env)->GetStringUTFChars(env, pwd, JNI_FALSE);
const char *_salt = (*env)->GetStringUTFChars(env, salt, JNI_FALSE);
jstring res = (*env)->NewStringUTF(env, chat_password_hash(_pwd, _salt));
(*env)->ReleaseStringUTFChars(env, pwd, _pwd);
(*env)->ReleaseStringUTFChars(env, salt, _salt);
return res;
}
@@ -3,9 +3,7 @@ package chat.simplex.app
import android.app.Application import android.app.Application
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.*
import android.os.Bundle
import android.os.Parcelable
import android.os.SystemClock.elapsedRealtime import android.os.SystemClock.elapsedRealtime
import android.util.Log import android.util.Log
import android.view.WindowManager import android.view.WindowManager
@@ -14,23 +12,22 @@ import androidx.activity.viewModels
import androidx.compose.animation.core.* import androidx.compose.animation.core.*
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.material.MaterialTheme import androidx.compose.material.*
import androidx.compose.material.Surface
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Lock
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.* import androidx.lifecycle.*
import chat.simplex.app.model.ChatModel import chat.simplex.app.MainActivity.Companion.enteredBackground
import chat.simplex.app.model.NtfManager import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.SimpleButton import chat.simplex.app.model.NtfManager.Companion.getUserIdFromIntent
import chat.simplex.app.ui.theme.SimpleXTheme import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.SplashView import chat.simplex.app.views.SplashView
import chat.simplex.app.views.call.ActiveCallView import chat.simplex.app.views.call.ActiveCallView
import chat.simplex.app.views.call.IncomingCallAlertView import chat.simplex.app.views.call.IncomingCallAlertView
@@ -38,11 +35,13 @@ import chat.simplex.app.views.chat.ChatView
import chat.simplex.app.views.chatlist.* import chat.simplex.app.views.chatlist.*
import chat.simplex.app.views.database.DatabaseErrorView import chat.simplex.app.views.database.DatabaseErrorView
import chat.simplex.app.views.helpers.* import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.helpers.DatabaseUtils.ksAppPassword
import chat.simplex.app.views.localauth.SetAppPasscodeView
import chat.simplex.app.views.newchat.* import chat.simplex.app.views.newchat.*
import chat.simplex.app.views.onboarding.* import chat.simplex.app.views.onboarding.*
import kotlinx.coroutines.delay import chat.simplex.app.views.usersettings.LAMode
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.launch
class MainActivity: FragmentActivity() { class MainActivity: FragmentActivity() {
companion object { companion object {
@@ -67,6 +66,7 @@ class MainActivity: FragmentActivity() {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
// testJson() // testJson()
val m = vm.chatModel val m = vm.chatModel
applyAppLocale(m.controller.appPrefs.appLanguage)
// When call ended and orientation changes, it re-process old intent, it's unneeded. // When call ended and orientation changes, it re-process old intent, it's unneeded.
// Only needed to be processed on first creation of activity // Only needed to be processed on first creation of activity
if (savedInstanceState == null) { if (savedInstanceState == null) {
@@ -83,20 +83,14 @@ class MainActivity: FragmentActivity() {
} }
setContent { setContent {
SimpleXTheme { SimpleXTheme {
Surface( MainPage(
Modifier m,
.background(MaterialTheme.colors.background) userAuthorized,
.fillMaxSize() laFailed,
) { ::runAuthenticate,
MainPage( ::setPerformLA,
m, showLANotice = { showLANotice(m.controller.appPrefs.laNoticeShown, this) }
userAuthorized, )
laFailed,
::runAuthenticate,
::setPerformLA,
showLANotice = { m.controller.showLANotice(this) }
)
}
} }
} }
SimplexApp.context.schedulePeriodicServiceRestartWorker() SimplexApp.context.schedulePeriodicServiceRestartWorker()
@@ -109,10 +103,11 @@ class MainActivity: FragmentActivity() {
processExternalIntent(intent, vm.chatModel) processExternalIntent(intent, vm.chatModel)
} }
override fun onStart() { override fun onResume() {
super.onStart() super.onResume()
val enteredBackgroundVal = enteredBackground.value val enteredBackgroundVal = enteredBackground.value
if (enteredBackgroundVal == null || elapsedRealtime() - enteredBackgroundVal >= 30_000) { val delay = vm.chatModel.controller.appPrefs.laLockDelay.get()
if (enteredBackgroundVal == null || elapsedRealtime() - enteredBackgroundVal >= delay * 1000) {
runAuthenticate() runAuthenticate()
} }
} }
@@ -129,6 +124,7 @@ class MainActivity: FragmentActivity() {
override fun onStop() { override fun onStop() {
super.onStop() super.onStop()
VideoPlayer.stopAll()
enteredBackground.value = elapsedRealtime() enteredBackground.value = elapsedRealtime()
} }
@@ -160,43 +156,155 @@ class MainActivity: FragmentActivity() {
} else { } else {
userAuthorized.value = false userAuthorized.value = false
ModalManager.shared.closeModals() ModalManager.shared.closeModals()
authenticate( // To make Main thread free in order to allow to Compose to show blank view that hiding content underneath of it faster on slow devices
generalGetString(R.string.auth_unlock), CoroutineScope(Dispatchers.Default).launch {
generalGetString(R.string.auth_log_in_using_credential), delay(50)
this@MainActivity, withContext(Dispatchers.Main) {
completed = { laResult -> authenticate(
when (laResult) { if (m.controller.appPrefs.laMode.get() == LAMode.SYSTEM)
LAResult.Success -> generalGetString(R.string.auth_unlock)
userAuthorized.value = true else
is LAResult.Error, LAResult.Failed -> generalGetString(R.string.la_enter_app_passcode),
laFailed.value = true if (m.controller.appPrefs.laMode.get() == LAMode.SYSTEM)
LAResult.Unavailable -> { generalGetString(R.string.auth_log_in_using_credential)
userAuthorized.value = true else
m.performLA.value = false generalGetString(R.string.auth_unlock),
m.controller.appPrefs.performLA.set(false) this@MainActivity,
laUnavailableTurningOffAlert() completed = { laResult ->
when (laResult) {
LAResult.Success ->
userAuthorized.value = true
is LAResult.Failed -> { /* Can be called multiple times on every failure */ }
is LAResult.Error -> {
laFailed.value = true
if (m.controller.appPrefs.laMode.get() == LAMode.PASSCODE) {
laFailedAlert()
}
}
is LAResult.Unavailable -> {
userAuthorized.value = true
m.performLA.value = false
m.controller.appPrefs.performLA.set(false)
laUnavailableTurningOffAlert()
}
}
} }
)
}
}
}
}
private fun showLANotice(laNoticeShown: SharedPreference<Boolean>, activity: FragmentActivity) {
Log.d(TAG, "showLANotice")
if (!laNoticeShown.get()) {
laNoticeShown.set(true)
AlertManager.shared.showAlertDialog(
title = generalGetString(R.string.la_notice_title_simplex_lock),
text = generalGetString(R.string.la_notice_to_protect_your_information_turn_on_simplex_lock_you_will_be_prompted_to_complete_authentication_before_this_feature_is_enabled),
confirmText = generalGetString(R.string.la_notice_turn_on),
onConfirm = {
withBGApi { // to remove this call, change ordering of onConfirm call in AlertManager
showChooseLAMode(laNoticeShown, activity)
} }
} }
) )
} }
} }
private fun setPerformLA(on: Boolean) { private fun showChooseLAMode(laNoticeShown: SharedPreference<Boolean>, activity: FragmentActivity) {
vm.chatModel.controller.appPrefs.laNoticeShown.set(true) Log.d(TAG, "showLANotice")
if (on) { laNoticeShown.set(true)
enableLA() AlertManager.shared.showAlertDialogStacked(
} else { title = generalGetString(R.string.la_lock_mode),
disableLA() text = null,
} confirmText = generalGetString(R.string.la_lock_mode_passcode),
dismissText = generalGetString(R.string.la_lock_mode_system),
onConfirm = {
AlertManager.shared.hideAlert()
setPasscode()
},
onDismiss = {
AlertManager.shared.hideAlert()
initialEnableLA(activity)
}
)
} }
private fun enableLA() { private fun initialEnableLA(activity: FragmentActivity) {
val m = vm.chatModel val m = vm.chatModel
val appPrefs = m.controller.appPrefs
m.controller.appPrefs.laMode.set(LAMode.SYSTEM)
authenticate( authenticate(
generalGetString(R.string.auth_enable_simplex_lock), generalGetString(R.string.auth_enable_simplex_lock),
generalGetString(R.string.auth_confirm_credential), generalGetString(R.string.auth_confirm_credential),
this@MainActivity, activity,
completed = { laResult ->
when (laResult) {
LAResult.Success -> {
m.performLA.value = true
appPrefs.performLA.set(true)
laTurnedOnAlert()
}
is LAResult.Failed -> { /* Can be called multiple times on every failure */ }
is LAResult.Error -> {
m.performLA.value = false
appPrefs.performLA.set(false)
laFailedAlert()
}
is LAResult.Unavailable -> {
m.performLA.value = false
appPrefs.performLA.set(false)
m.showAdvertiseLAUnavailableAlert.value = true
}
}
}
)
}
private fun setPasscode() {
val chatModel = vm.chatModel
val appPrefs = chatModel.controller.appPrefs
ModalManager.shared.showCustomModal { close ->
Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background) {
SetAppPasscodeView(
submit = {
chatModel.performLA.value = true
appPrefs.performLA.set(true)
appPrefs.laMode.set(LAMode.PASSCODE)
laTurnedOnAlert()
},
cancel = {
chatModel.performLA.value = false
appPrefs.performLA.set(false)
laPasscodeNotSetAlert()
},
close)
}
}
}
private fun setPerformLA(on: Boolean, activity: FragmentActivity) {
vm.chatModel.controller.appPrefs.laNoticeShown.set(true)
if (on) {
enableLA(activity)
} else {
disableLA(activity)
}
}
private fun enableLA(activity: FragmentActivity) {
val m = vm.chatModel
authenticate(
if (m.controller.appPrefs.laMode.get() == LAMode.SYSTEM)
generalGetString(R.string.auth_enable_simplex_lock)
else
generalGetString(R.string.new_passcode),
if (m.controller.appPrefs.laMode.get() == LAMode.SYSTEM)
generalGetString(R.string.auth_confirm_credential)
else
"",
activity,
completed = { laResult -> completed = { laResult ->
val prefPerformLA = m.controller.appPrefs.performLA val prefPerformLA = m.controller.appPrefs.performLA
when (laResult) { when (laResult) {
@@ -205,11 +313,13 @@ class MainActivity: FragmentActivity() {
prefPerformLA.set(true) prefPerformLA.set(true)
laTurnedOnAlert() laTurnedOnAlert()
} }
is LAResult.Error, LAResult.Failed -> { is LAResult.Failed -> { /* Can be called multiple times on every failure */ }
is LAResult.Error -> {
m.performLA.value = false m.performLA.value = false
prefPerformLA.set(false) prefPerformLA.set(false)
laFailedAlert()
} }
LAResult.Unavailable -> { is LAResult.Unavailable -> {
m.performLA.value = false m.performLA.value = false
prefPerformLA.set(false) prefPerformLA.set(false)
laUnavailableInstructionAlert() laUnavailableInstructionAlert()
@@ -219,24 +329,33 @@ class MainActivity: FragmentActivity() {
) )
} }
private fun disableLA() { private fun disableLA(activity: FragmentActivity) {
val m = vm.chatModel val m = vm.chatModel
authenticate( authenticate(
generalGetString(R.string.auth_disable_simplex_lock), if (m.controller.appPrefs.laMode.get() == LAMode.SYSTEM)
generalGetString(R.string.auth_confirm_credential), generalGetString(R.string.auth_disable_simplex_lock)
this@MainActivity, else
generalGetString(R.string.la_enter_app_passcode),
if (m.controller.appPrefs.laMode.get() == LAMode.SYSTEM)
generalGetString(R.string.auth_confirm_credential)
else
generalGetString(R.string.auth_disable_simplex_lock),
activity,
completed = { laResult -> completed = { laResult ->
val prefPerformLA = m.controller.appPrefs.performLA val prefPerformLA = m.controller.appPrefs.performLA
when (laResult) { when (laResult) {
LAResult.Success -> { LAResult.Success -> {
m.performLA.value = false m.performLA.value = false
prefPerformLA.set(false) prefPerformLA.set(false)
ksAppPassword.remove()
} }
is LAResult.Error, LAResult.Failed -> { is LAResult.Failed -> { /* Can be called multiple times on every failure */ }
is LAResult.Error -> {
m.performLA.value = true m.performLA.value = true
prefPerformLA.set(true) prefPerformLA.set(true)
laFailedAlert()
} }
LAResult.Unavailable -> { is LAResult.Unavailable -> {
m.performLA.value = false m.performLA.value = false
prefPerformLA.set(false) prefPerformLA.set(false)
laUnavailableTurningOffAlert() laUnavailableTurningOffAlert()
@@ -258,17 +377,9 @@ fun MainPage(
userAuthorized: MutableState<Boolean?>, userAuthorized: MutableState<Boolean?>,
laFailed: MutableState<Boolean>, laFailed: MutableState<Boolean>,
runAuthenticate: () -> Unit, runAuthenticate: () -> Unit,
setPerformLA: (Boolean) -> Unit, setPerformLA: (Boolean, FragmentActivity) -> Unit,
showLANotice: () -> Unit showLANotice: () -> Unit
) { ) {
// this with LaunchedEffect(userAuthorized.value) fixes bottom sheet visibly collapsing after authentication
var chatsAccessAuthorized by rememberSaveable { mutableStateOf(false) }
LaunchedEffect(userAuthorized.value) {
if (chatModel.controller.appPrefs.performLA.get()) {
delay(500L)
}
chatsAccessAuthorized = userAuthorized.value == true
}
var showChatDatabaseError by rememberSaveable { var showChatDatabaseError by rememberSaveable {
mutableStateOf(chatModel.chatDbStatus.value != DBMigrationResult.OK && chatModel.chatDbStatus.value != null) mutableStateOf(chatModel.chatDbStatus.value != DBMigrationResult.OK && chatModel.chatDbStatus.value != null)
} }
@@ -303,12 +414,12 @@ fun MainPage(
@Composable @Composable
fun authView() { fun authView() {
Box( Box(
Modifier.fillMaxSize(), Modifier.fillMaxSize().background(MaterialTheme.colors.background),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
SimpleButton( SimpleButton(
stringResource(R.string.auth_unlock), stringResource(R.string.auth_unlock),
icon = Icons.Outlined.Lock, icon = painterResource(R.drawable.ic_lock),
click = { click = {
laFailed.value = false laFailed.value = false
runAuthenticate() runAuthenticate()
@@ -327,7 +438,7 @@ fun MainPage(
} }
} }
onboarding == null || userCreated == null -> SplashView() onboarding == null || userCreated == null -> SplashView()
!chatsAccessAuthorized -> { userAuthorized.value != true -> {
if (chatModel.controller.appPrefs.performLA.get() && laFailed.value) { if (chatModel.controller.appPrefs.performLA.get() && laFailed.value) {
authView() authView()
} else { } else {
@@ -373,13 +484,6 @@ fun MainPage(
.collect { .collect {
if (it != null) currentChatId = it if (it != null) currentChatId = it
else onComposed() else onComposed()
// Deletes files that were not sent but already stored in files directory.
// Currently, it's voice records only
if (it == null && chatModel.filesToDelete.isNotEmpty()) {
chatModel.filesToDelete.forEach { it.delete() }
chatModel.filesToDelete.clear()
}
} }
} }
} }
@@ -393,7 +497,7 @@ fun MainPage(
} }
} }
onboarding == OnboardingStage.Step1_SimpleXInfo -> SimpleXInfo(chatModel, onboarding = true) onboarding == OnboardingStage.Step1_SimpleXInfo -> SimpleXInfo(chatModel, onboarding = true)
onboarding == OnboardingStage.Step2_CreateProfile -> CreateProfile(chatModel) onboarding == OnboardingStage.Step2_CreateProfile -> CreateProfile(chatModel) {}
onboarding == OnboardingStage.Step3_SetNotificationsMode -> SetNotificationsMode(chatModel) onboarding == OnboardingStage.Step3_SetNotificationsMode -> SetNotificationsMode(chatModel)
} }
ModalManager.shared.showInView() ModalManager.shared.showInView()
@@ -401,23 +505,44 @@ fun MainPage(
if (invitation != null) IncomingCallAlertView(invitation, chatModel) if (invitation != null) IncomingCallAlertView(invitation, chatModel)
AlertManager.shared.showInView() AlertManager.shared.showInView()
} }
DisposableEffectOnRotate {
// When using lock delay = 0 and screen rotates, the app will be locked which is not useful.
// Let's prolong the unlocked period to 3 sec for screen rotation to take place
if (chatModel.controller.appPrefs.laLockDelay.get() == 0) {
enteredBackground.value = elapsedRealtime() + 3000
}
}
} }
fun processNotificationIntent(intent: Intent?, chatModel: ChatModel) { fun processNotificationIntent(intent: Intent?, chatModel: ChatModel) {
val userId = getUserIdFromIntent(intent)
when (intent?.action) { when (intent?.action) {
NtfManager.OpenChatAction -> { NtfManager.OpenChatAction -> {
val chatId = intent.getStringExtra("chatId") val chatId = intent.getStringExtra("chatId")
Log.d(TAG, "processNotificationIntent: OpenChatAction $chatId") Log.d(TAG, "processNotificationIntent: OpenChatAction $chatId")
if (chatId != null) { if (chatId != null) {
val cInfo = chatModel.getChat(chatId)?.chatInfo withBGApi {
chatModel.clearOverlays.value = true awaitChatStartedIfNeeded(chatModel)
if (cInfo != null) withApi { openChat(cInfo, chatModel) } if (userId != null && userId != chatModel.currentUser.value?.userId && chatModel.currentUser.value != null) {
chatModel.controller.changeActiveUser(userId, null)
}
val cInfo = chatModel.getChat(chatId)?.chatInfo
chatModel.clearOverlays.value = true
if (cInfo != null) openChat(cInfo, chatModel)
}
} }
} }
NtfManager.ShowChatsAction -> { NtfManager.ShowChatsAction -> {
Log.d(TAG, "processNotificationIntent: ShowChatsAction") Log.d(TAG, "processNotificationIntent: ShowChatsAction")
chatModel.chatId.value = null withBGApi {
chatModel.clearOverlays.value = true awaitChatStartedIfNeeded(chatModel)
if (userId != null && userId != chatModel.currentUser.value?.userId && chatModel.currentUser.value != null) {
chatModel.controller.changeActiveUser(userId, null)
}
chatModel.chatId.value = null
chatModel.clearOverlays.value = true
}
} }
NtfManager.AcceptCallAction -> { NtfManager.AcceptCallAction -> {
val chatId = intent.getStringExtra("chatId") val chatId = intent.getStringExtra("chatId")
@@ -450,14 +575,23 @@ fun processExternalIntent(intent: Intent?, chatModel: ChatModel) {
chatModel.chatId.value = null chatModel.chatId.value = null
chatModel.clearOverlays.value = true chatModel.clearOverlays.value = true
when { when {
"text/plain" == intent.type -> intent.getStringExtra(Intent.EXTRA_TEXT)?.let { intent.type == "text/plain" -> {
chatModel.sharedContent.value = SharedContent.Text(it) val text = intent.getStringExtra(Intent.EXTRA_TEXT)
if (text != null) {
chatModel.sharedContent.value = SharedContent.Text(text)
}
} }
intent.type?.startsWith("image/") == true -> (intent.getParcelableExtra<Parcelable>(Intent.EXTRA_STREAM) as? Uri)?.let { isMediaIntent(intent) -> {
chatModel.sharedContent.value = SharedContent.Images(intent.getStringExtra(Intent.EXTRA_TEXT) ?: "", listOf(it)) val uri = intent.getParcelableExtra<Parcelable>(Intent.EXTRA_STREAM) as? Uri
} // All other mime types if (uri != null) {
else -> (intent.getParcelableExtra<Parcelable>(Intent.EXTRA_STREAM) as? Uri)?.let { chatModel.sharedContent.value = SharedContent.Media(intent.getStringExtra(Intent.EXTRA_TEXT) ?: "", listOf(uri))
chatModel.sharedContent.value = SharedContent.File(intent.getStringExtra(Intent.EXTRA_TEXT) ?: "", it) } // All other mime types
}
else -> {
val uri = intent.getParcelableExtra<Parcelable>(Intent.EXTRA_STREAM) as? Uri
if (uri != null) {
chatModel.sharedContent.value = SharedContent.File(intent.getStringExtra(Intent.EXTRA_TEXT) ?: "", uri)
}
} }
} }
} }
@@ -465,16 +599,23 @@ fun processExternalIntent(intent: Intent?, chatModel: ChatModel) {
// Close active chat and show a list of chats // Close active chat and show a list of chats
chatModel.chatId.value = null chatModel.chatId.value = null
chatModel.clearOverlays.value = true chatModel.clearOverlays.value = true
Log.e(TAG, "ACTION_SEND_MULTIPLE ${intent.type}")
when { when {
intent.type?.startsWith("image/") == true -> (intent.getParcelableArrayListExtra<Parcelable>(Intent.EXTRA_STREAM) as? List<Uri>)?.let { isMediaIntent(intent) -> {
chatModel.sharedContent.value = SharedContent.Images(intent.getStringExtra(Intent.EXTRA_TEXT) ?: "", it) val uris = intent.getParcelableArrayListExtra<Parcelable>(Intent.EXTRA_STREAM) as? List<Uri>
} // All other mime types if (uris != null) {
chatModel.sharedContent.value = SharedContent.Media(intent.getStringExtra(Intent.EXTRA_TEXT) ?: "", uris)
} // All other mime types
}
else -> {} else -> {}
} }
} }
} }
} }
fun isMediaIntent(intent: Intent): Boolean =
intent.type?.startsWith("image/") == true || intent.type?.startsWith("video/") == true
fun connectIfOpenedViaUri(uri: Uri, chatModel: ChatModel) { fun connectIfOpenedViaUri(uri: Uri, chatModel: ChatModel) {
Log.d(TAG, "connectIfOpenedViaUri: opened via link") Log.d(TAG, "connectIfOpenedViaUri: opened via link")
if (chatModel.currentUser.value == null) { if (chatModel.currentUser.value == null) {
@@ -486,7 +627,7 @@ fun connectIfOpenedViaUri(uri: Uri, chatModel: ChatModel) {
ConnectionLinkType.INVITATION -> generalGetString(R.string.connect_via_invitation_link) ConnectionLinkType.INVITATION -> generalGetString(R.string.connect_via_invitation_link)
ConnectionLinkType.GROUP -> generalGetString(R.string.connect_via_group_link) ConnectionLinkType.GROUP -> generalGetString(R.string.connect_via_group_link)
} }
AlertManager.shared.showAlertMsg( AlertManager.shared.showAlertDialog(
title = title, title = title,
text = if (linkType == ConnectionLinkType.GROUP) text = if (linkType == ConnectionLinkType.GROUP)
generalGetString(R.string.you_will_join_group) generalGetString(R.string.you_will_join_group)
@@ -503,6 +644,20 @@ fun connectIfOpenedViaUri(uri: Uri, chatModel: ChatModel) {
} }
} }
} }
suspend fun awaitChatStartedIfNeeded(chatModel: ChatModel, timeout: Long = 30_000) {
// Still decrypting database
if (chatModel.chatRunning.value == null) {
val step = 50L
for (i in 0..(timeout / step)) {
if (chatModel.chatRunning.value == true || chatModel.onboardingStage.value == OnboardingStage.Step1_SimpleXInfo) {
break
}
delay(step)
}
}
}
//fun testJson() { //fun testJson() {
// val str: String = """ // val str: String = """
// """.trimIndent() // """.trimIndent()
@@ -6,6 +6,7 @@ import android.util.Log
import androidx.lifecycle.* import androidx.lifecycle.*
import androidx.work.* import androidx.work.*
import chat.simplex.app.model.* import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.DefaultTheme
import chat.simplex.app.views.helpers.* import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.onboarding.OnboardingStage import chat.simplex.app.views.onboarding.OnboardingStage
import chat.simplex.app.views.usersettings.NotificationsMode import chat.simplex.app.views.usersettings.NotificationsMode
@@ -26,22 +27,26 @@ external fun pipeStdOutToSocket(socketName: String) : Int
// SimpleX API // SimpleX API
typealias ChatCtrl = Long typealias ChatCtrl = Long
external fun chatMigrateInit(dbPath: String, dbKey: String): Array<Any> external fun chatMigrateInit(dbPath: String, dbKey: String, confirm: String): Array<Any>
external fun chatSendCmd(ctrl: ChatCtrl, msg: String): String external fun chatSendCmd(ctrl: ChatCtrl, msg: String): String
external fun chatRecvMsg(ctrl: ChatCtrl): String external fun chatRecvMsg(ctrl: ChatCtrl): String
external fun chatRecvMsgWait(ctrl: ChatCtrl, timeout: Int): String external fun chatRecvMsgWait(ctrl: ChatCtrl, timeout: Int): String
external fun chatParseMarkdown(str: String): String external fun chatParseMarkdown(str: String): String
external fun chatParseServer(str: String): String external fun chatParseServer(str: String): String
external fun chatPasswordHash(pwd: String, salt: String): String
class SimplexApp: Application(), LifecycleEventObserver { class SimplexApp: Application(), LifecycleEventObserver {
lateinit var chatController: ChatController lateinit var chatController: ChatController
var isAppOnForeground: Boolean = false var isAppOnForeground: Boolean = false
fun initChatController(useKey: String? = null, startChat: Boolean = true) { val defaultLocale: Locale = Locale.getDefault()
fun initChatController(useKey: String? = null, confirmMigrations: MigrationConfirmation? = null, startChat: Boolean = true) {
val dbKey = useKey ?: DatabaseUtils.useDatabaseKey() val dbKey = useKey ?: DatabaseUtils.useDatabaseKey()
val dbAbsolutePathPrefix = getFilesDirectory(SimplexApp.context) val dbAbsolutePathPrefix = getFilesDirectory(SimplexApp.context)
val migrated: Array<Any> = chatMigrateInit(dbAbsolutePathPrefix, dbKey) val confirm = confirmMigrations ?: if (appPreferences.confirmDBUpgrades.get()) MigrationConfirmation.Error else MigrationConfirmation.YesUp
val migrated: Array<Any> = chatMigrateInit(dbAbsolutePathPrefix, dbKey, confirm.value)
val res: DBMigrationResult = kotlin.runCatching { val res: DBMigrationResult = kotlin.runCatching {
json.decodeFromString<DBMigrationResult>(migrated[0] as String) json.decodeFromString<DBMigrationResult>(migrated[0] as String)
}.getOrElse { DBMigrationResult.Unknown(migrated[0] as String) } }.getOrElse { DBMigrationResult.Unknown(migrated[0] as String) }
@@ -90,6 +95,8 @@ class SimplexApp: Application(), LifecycleEventObserver {
context = this context = this
initChatController() initChatController()
ProcessLifecycleOwner.get().lifecycle.addObserver(this) ProcessLifecycleOwner.get().lifecycle.addObserver(this)
context.getDir("temp", MODE_PRIVATE).deleteRecursively()
runMigrations()
} }
override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) { override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
@@ -100,8 +107,19 @@ class SimplexApp: Application(), LifecycleEventObserver {
isAppOnForeground = true isAppOnForeground = true
if (chatModel.chatRunning.value == true) { if (chatModel.chatRunning.value == true) {
kotlin.runCatching { kotlin.runCatching {
val chats = chatController.apiGetChats() val currentUserId = chatModel.currentUser.value?.userId
chatModel.updateChats(chats) val chats = ArrayList(chatController.apiGetChats())
/** Active user can be changed in background while [ChatController.apiGetChats] is executing */
if (chatModel.currentUser.value?.userId == currentUserId) {
val currentChatId = chatModel.chatId.value
val oldStats = if (currentChatId != null) chatModel.getChat(currentChatId)?.chatStats else null
if (oldStats != null) {
val indexOfCurrentChat = chats.indexOfFirst { it.id == currentChatId }
/** Pass old chatStats because unreadCounter can be changed already while [ChatController.apiGetChats] is executing */
if (indexOfCurrentChat >= 0) chats[indexOfCurrentChat] = chats[indexOfCurrentChat].copy(chatStats = oldStats)
}
chatModel.updateChats(chats)
}
}.onFailure { Log.e(TAG, it.stackTraceToString()) } }.onFailure { Log.e(TAG, it.stackTraceToString()) }
} }
} }
@@ -168,6 +186,23 @@ class SimplexApp: Application(), LifecycleEventObserver {
MessagesFetcherWorker.scheduleWork() MessagesFetcherWorker.scheduleWork()
} }
private fun runMigrations() {
val lastMigration = chatModel.controller.appPrefs.lastMigratedVersionCode
if (lastMigration.get() < BuildConfig.VERSION_CODE) {
while (true) {
if (lastMigration.get() < 117) {
if (chatModel.controller.appPrefs.currentTheme.get() == DefaultTheme.DARK.name) {
chatModel.controller.appPrefs.currentTheme.set(DefaultTheme.SIMPLEX.name)
}
lastMigration.set(117)
} else {
lastMigration.set(BuildConfig.VERSION_CODE)
break
}
}
}
}
companion object { companion object {
lateinit var context: SimplexApp private set lateinit var context: SimplexApp private set
@@ -326,4 +326,4 @@ class SimplexService: Service() {
private fun getPreferences(context: Context): SharedPreferences = context.getSharedPreferences(SHARED_PREFS_ID, Context.MODE_PRIVATE) private fun getPreferences(context: Context): SharedPreferences = context.getSharedPreferences(SHARED_PREFS_ID, Context.MODE_PRIVATE)
} }
} }
@@ -2,17 +2,17 @@ package chat.simplex.app.model
import android.net.Uri import android.net.Uri
import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.font.* import androidx.compose.ui.text.font.*
import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.text.style.TextDecoration
import chat.simplex.app.R import chat.simplex.app.R
import chat.simplex.app.ui.theme.* import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.call.* import chat.simplex.app.views.call.*
import chat.simplex.app.views.chat.ComposeState
import chat.simplex.app.views.helpers.* import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.onboarding.OnboardingStage import chat.simplex.app.views.onboarding.OnboardingStage
import chat.simplex.app.views.usersettings.NotificationPreviewMode import chat.simplex.app.views.usersettings.NotificationPreviewMode
@@ -35,6 +35,7 @@ import kotlin.time.*
class ChatModel(val controller: ChatController) { class ChatModel(val controller: ChatController) {
val onboardingStage = mutableStateOf<OnboardingStage?>(null) val onboardingStage = mutableStateOf<OnboardingStage?>(null)
val currentUser = mutableStateOf<User?>(null) val currentUser = mutableStateOf<User?>(null)
val users = mutableStateListOf<UserInfo>()
val userCreated = mutableStateOf<Boolean?>(null) val userCreated = mutableStateOf<Boolean?>(null)
val chatRunning = mutableStateOf<Boolean?>(null) val chatRunning = mutableStateOf<Boolean?>(null)
val chatDbChanged = mutableStateOf<Boolean>(false) val chatDbChanged = mutableStateOf<Boolean>(false)
@@ -42,6 +43,8 @@ class ChatModel(val controller: ChatController) {
val chatDbStatus = mutableStateOf<DBMigrationResult?>(null) val chatDbStatus = mutableStateOf<DBMigrationResult?>(null)
val chatDbDeleted = mutableStateOf(false) val chatDbDeleted = mutableStateOf(false)
val chats = mutableStateListOf<Chat>() val chats = mutableStateListOf<Chat>()
// map of connections network statuses, key is agent connection id
val networkStatuses = mutableStateMapOf<String, NetworkStatus>()
// current chat // current chat
val chatId = mutableStateOf<String?>(null) val chatId = mutableStateOf<String?>(null)
@@ -50,10 +53,8 @@ class ChatModel(val controller: ChatController) {
val terminalItems = mutableStateListOf<TerminalItem>() val terminalItems = mutableStateListOf<TerminalItem>()
val userAddress = mutableStateOf<UserContactLinkRec?>(null) val userAddress = mutableStateOf<UserContactLinkRec?>(null)
val userSMPServers = mutableStateOf<(List<ServerCfg>)?>(null)
// Allows to temporary save servers that are being edited on multiple screens // Allows to temporary save servers that are being edited on multiple screens
val userSMPServersUnsaved = mutableStateOf<(List<ServerCfg>)?>(null) val userSMPServersUnsaved = mutableStateOf<(List<ServerCfg>)?>(null)
val presetSMPServers = mutableStateOf<(List<String>)?>(null)
val chatItemTTL = mutableStateOf<ChatItemTTL>(ChatItemTTL.None) val chatItemTTL = mutableStateOf<ChatItemTTL>(ChatItemTTL.None)
// set when app opened from external intent // set when app opened from external intent
@@ -81,16 +82,38 @@ class ChatModel(val controller: ChatController) {
// currently showing QR code // currently showing QR code
val connReqInv = mutableStateOf(null as String?) val connReqInv = mutableStateOf(null as String?)
var draft = mutableStateOf(null as ComposeState?)
var draftChatId = mutableStateOf(null as String?)
// working with external intents // working with external intents
val sharedContent = mutableStateOf(null as SharedContent?) val sharedContent = mutableStateOf(null as SharedContent?)
val filesToDelete = mutableSetOf<File>() val filesToDelete = mutableSetOf<File>()
val simplexLinkMode = mutableStateOf(controller.appPrefs.simplexLinkMode.get()) val simplexLinkMode = mutableStateOf(controller.appPrefs.simplexLinkMode.get())
fun updateUserProfile(profile: LocalProfile) { fun getUser(userId: Long): User? = if (currentUser.value?.userId == userId) {
val user = currentUser.value currentUser.value
if (user != null) { } else {
currentUser.value = user.copy(profile = profile) users.firstOrNull { it.user.userId == userId }?.user
}
private fun getUserIndex(user: User): Int =
users.indexOfFirst { it.user.userId == user.userId }
fun updateUser(user: User) {
val i = getUserIndex(user)
if (i != -1) {
users[i] = users[i].copy(user = user)
}
if (currentUser.value?.userId == user.userId) {
currentUser.value = user
}
}
fun removeUser(user: User) {
val i = getUserIndex(user)
if (i != -1 && users[i].user.userId != currentUser.value?.userId) {
users.removeAt(i)
} }
} }
@@ -120,17 +143,8 @@ class ChatModel(val controller: ChatController) {
} }
fun updateChats(newChats: List<Chat>) { fun updateChats(newChats: List<Chat>) {
val mergedChats = arrayListOf<Chat>()
for (newChat in newChats) {
val i = getChatIndex(newChat.chatInfo.id)
if (i >= 0) {
mergedChats.add(newChat.copy(serverInfo = chats[i].serverInfo))
} else {
mergedChats.add(newChat)
}
}
chats.clear() chats.clear()
chats.addAll(mergedChats) chats.addAll(newChats)
val cId = chatId.value val cId = chatId.value
// If chat is null, it was deleted in background after apiGetChats call // If chat is null, it was deleted in background after apiGetChats call
@@ -139,14 +153,6 @@ class ChatModel(val controller: ChatController) {
} }
} }
fun updateNetworkStatus(id: ChatId, status: Chat.NetworkStatus) {
val i = getChatIndex(id)
if (i >= 0) {
val chat = chats[i]
chats[i] = chat.copy(serverInfo = chat.serverInfo.copy(networkStatus = status))
}
}
fun replaceChat(id: String, chat: Chat) { fun replaceChat(id: String, chat: Chat) {
val i = getChatIndex(id) val i = getChatIndex(id)
if (i >= 0) { if (i >= 0) {
@@ -168,6 +174,7 @@ class ChatModel(val controller: ChatController) {
chatStats = chatStats =
if (cItem.meta.itemStatus is CIStatus.RcvNew) { if (cItem.meta.itemStatus is CIStatus.RcvNew) {
val minUnreadId = if(chat.chatStats.minUnreadItemId == 0L) cItem.id else chat.chatStats.minUnreadItemId val minUnreadId = if(chat.chatStats.minUnreadItemId == 0L) cItem.id else chat.chatStats.minUnreadItemId
increaseUnreadCounter(currentUser.value!!)
chat.chatStats.copy(unreadCount = chat.chatStats.unreadCount + 1, minUnreadItemId = minUnreadId) chat.chatStats.copy(unreadCount = chat.chatStats.unreadCount + 1, minUnreadItemId = minUnreadId)
} }
else else
@@ -182,10 +189,13 @@ class ChatModel(val controller: ChatController) {
// add to current chat // add to current chat
if (chatId.value == cInfo.id) { if (chatId.value == cInfo.id) {
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
if (chatItems.lastOrNull()?.id == ChatItem.TEMP_LIVE_CHAT_ITEM_ID) { // Prevent situation when chat item already in the list received from backend
chatItems.add(kotlin.math.max(0, chatItems.lastIndex), cItem) if (chatItems.none { it.id == cItem.id }) {
} else { if (chatItems.lastOrNull()?.id == ChatItem.TEMP_LIVE_CHAT_ITEM_ID) {
chatItems.add(cItem) chatItems.add(kotlin.math.max(0, chatItems.lastIndex), cItem)
} else {
chatItems.add(cItem)
}
} }
} }
} }
@@ -212,19 +222,19 @@ class ChatModel(val controller: ChatController) {
res = true res = true
} }
// update current chat // update current chat
if (chatId.value == cInfo.id) { return if (chatId.value == cInfo.id) {
val itemIndex = chatItems.indexOfFirst { it.id == cItem.id } withContext(Dispatchers.Main) {
if (itemIndex >= 0) { val itemIndex = chatItems.indexOfFirst { it.id == cItem.id }
chatItems[itemIndex] = cItem if (itemIndex >= 0) {
return false chatItems[itemIndex] = cItem
} else { false
withContext(Dispatchers.Main) { } else {
chatItems.add(cItem) chatItems.add(cItem)
true
} }
return true
} }
} else { } else {
return res res
} }
} }
@@ -256,6 +266,7 @@ class ChatModel(val controller: ChatController) {
// clear preview // clear preview
val i = getChatIndex(cInfo.id) val i = getChatIndex(cInfo.id)
if (i >= 0) { if (i >= 0) {
decreaseUnreadCounter(currentUser.value!!, chats[i].chatStats.unreadCount)
chats[i] = chats[i].copy(chatItems = arrayListOf(), chatStats = Chat.ChatStats(), chatInfo = cInfo) chats[i] = chats[i].copy(chatItems = arrayListOf(), chatStats = Chat.ChatStats(), chatInfo = cInfo)
} }
// clear current chat // clear current chat
@@ -264,6 +275,19 @@ class ChatModel(val controller: ChatController) {
} }
} }
fun updateCurrentUser(newProfile: Profile, preferences: FullChatPreferences? = null) {
val current = currentUser.value ?: return
val updated = current.copy(
profile = newProfile.toLocalProfile(current.profile.profileId),
fullPreferences = preferences ?: current.fullPreferences
)
val indexInUsers = users.indexOfFirst { it.user.userId == current.userId }
if (indexInUsers != -1) {
users[indexInUsers] = UserInfo(updated, users[indexInUsers].unreadCount)
}
currentUser.value = updated
}
suspend fun addLiveDummy(chatInfo: ChatInfo): ChatItem { suspend fun addLiveDummy(chatInfo: ChatInfo): ChatItem {
val cItem = ChatItem.liveDummy(chatInfo is ChatInfo.Direct) val cItem = ChatItem.liveDummy(chatInfo is ChatInfo.Direct)
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
@@ -286,9 +310,11 @@ class ChatModel(val controller: ChatController) {
val chat = chats[chatIdx] val chat = chats[chatIdx]
val lastId = chat.chatItems.lastOrNull()?.id val lastId = chat.chatItems.lastOrNull()?.id
if (lastId != null) { if (lastId != null) {
val unreadCount = unreadCountAfter ?: if (range != null) chat.chatStats.unreadCount - markedRead else 0
decreaseUnreadCounter(currentUser.value!!, chat.chatStats.unreadCount - unreadCount)
chats[chatIdx] = chat.copy( chats[chatIdx] = chat.copy(
chatStats = chat.chatStats.copy( chatStats = chat.chatStats.copy(
unreadCount = unreadCountAfter ?: if (range != null) chat.chatStats.unreadCount - markedRead else 0, unreadCount = unreadCount,
// Can't use minUnreadItemId currently since chat items can have unread items between read items // Can't use minUnreadItemId currently since chat items can have unread items between read items
//minUnreadItemId = if (range != null) kotlin.math.max(chat.chatStats.minUnreadItemId, range.to + 1) else lastId + 1 //minUnreadItemId = if (range != null) kotlin.math.max(chat.chatStats.minUnreadItemId, range.to + 1) else lastId + 1
) )
@@ -324,13 +350,30 @@ class ChatModel(val controller: ChatController) {
if (chatIndex == -1) return if (chatIndex == -1) return
val chat = chats[chatIndex] val chat = chats[chatIndex]
val unreadCount = kotlin.math.max(chat.chatStats.unreadCount - 1, 0)
decreaseUnreadCounter(currentUser.value!!, chat.chatStats.unreadCount - unreadCount)
chats[chatIndex] = chat.copy( chats[chatIndex] = chat.copy(
chatStats = chat.chatStats.copy( chatStats = chat.chatStats.copy(
unreadCount = kotlin.math.max(chat.chatStats.unreadCount - 1, 0), unreadCount = unreadCount,
) )
) )
} }
fun increaseUnreadCounter(user: User) {
changeUnreadCounter(user, 1)
}
fun decreaseUnreadCounter(user: User, by: Int = 1) {
changeUnreadCounter(user, -by)
}
private fun changeUnreadCounter(user: User, by: Int) {
val i = users.indexOfFirst { it.user.userId == user.userId }
if (i != -1) {
users[i] = UserInfo(user, users[i].unreadCount + by)
}
}
// func popChat(_ id: String) { // func popChat(_ id: String) {
// if let i = getChatIndex(id) { // if let i = getChatIndex(id) {
// popChat_(i) // popChat_(i)
@@ -375,6 +418,20 @@ class ChatModel(val controller: ChatController) {
false false
} }
} }
fun setContactNetworkStatus(contact: Contact, status: NetworkStatus) {
networkStatuses[contact.activeConn.agentConnId] = status
}
fun contactNetworkStatus(contact: Contact): NetworkStatus =
networkStatuses[contact.activeConn.agentConnId] ?: NetworkStatus.Unknown()
fun addTerminalItem(item: TerminalItem) {
if (terminalItems.size >= 500) {
terminalItems.removeAt(0)
}
terminalItems.add(item)
}
} }
enum class ChatType(val type: String) { enum class ChatType(val type: String) {
@@ -391,13 +448,19 @@ data class User(
val localDisplayName: String, val localDisplayName: String,
val profile: LocalProfile, val profile: LocalProfile,
val fullPreferences: FullChatPreferences, val fullPreferences: FullChatPreferences,
val activeUser: Boolean val activeUser: Boolean,
val showNtfs: Boolean,
val viewPwdHash: UserPwdHash?
): NamedChat { ): NamedChat {
override val displayName: String get() = profile.displayName override val displayName: String get() = profile.displayName
override val fullName: String get() = profile.fullName override val fullName: String get() = profile.fullName
override val image: String? get() = profile.image override val image: String? get() = profile.image
override val localAlias: String = "" override val localAlias: String = ""
val hidden: Boolean = viewPwdHash != null
val showNotifications: Boolean = activeUser || showNtfs
companion object { companion object {
val sampleData = User( val sampleData = User(
userId = 1, userId = 1,
@@ -405,7 +468,28 @@ data class User(
localDisplayName = "alice", localDisplayName = "alice",
profile = LocalProfile.sampleData, profile = LocalProfile.sampleData,
fullPreferences = FullChatPreferences.sampleData, fullPreferences = FullChatPreferences.sampleData,
activeUser = true activeUser = true,
showNtfs = true,
viewPwdHash = null,
)
}
}
@Serializable
data class UserPwdHash(
val hash: String,
val salt: String
)
@Serializable
data class UserInfo(
val user: User,
val unreadCount: Int
) {
companion object {
val sampleData = UserInfo(
user = User.sampleData,
unreadCount = 1
) )
} }
} }
@@ -441,37 +525,30 @@ data class Chat (
val chatInfo: ChatInfo, val chatInfo: ChatInfo,
val chatItems: List<ChatItem>, val chatItems: List<ChatItem>,
val chatStats: ChatStats = ChatStats(), val chatStats: ChatStats = ChatStats(),
val serverInfo: ServerInfo = ServerInfo(NetworkStatus.Unknown())
) { ) {
val userCanSend: Boolean
get() = when (chatInfo) {
is ChatInfo.Direct -> true
is ChatInfo.Group -> {
val m = chatInfo.groupInfo.membership
m.memberActive && m.memberRole >= GroupMemberRole.Member
}
else -> false
}
val userIsObserver: Boolean get() = when(chatInfo) {
is ChatInfo.Group -> {
val m = chatInfo.groupInfo.membership
m.memberActive && m.memberRole == GroupMemberRole.Observer
}
else -> false
}
val id: String get() = chatInfo.id val id: String get() = chatInfo.id
@Serializable @Serializable
data class ChatStats(val unreadCount: Int = 0, val minUnreadItemId: Long = 0, val unreadChat: Boolean = false) data class ChatStats(val unreadCount: Int = 0, val minUnreadItemId: Long = 0, val unreadChat: Boolean = false)
@Serializable
data class ServerInfo(val networkStatus: NetworkStatus)
@Serializable
sealed class NetworkStatus {
val statusString: String get() =
when (this) {
is Connected -> generalGetString(R.string.server_connected)
is Error -> generalGetString(R.string.server_error)
else -> generalGetString(R.string.server_connecting)
}
val statusExplanation: String get() =
when (this) {
is Connected -> generalGetString(R.string.connected_to_server_to_receive_messages_from_contact)
is Error -> String.format(generalGetString(R.string.trying_to_connect_to_server_to_receive_messages_with_error), error)
else -> generalGetString(R.string.trying_to_connect_to_server_to_receive_messages)
}
@Serializable @SerialName("unknown") class Unknown: NetworkStatus()
@Serializable @SerialName("connected") class Connected: NetworkStatus()
@Serializable @SerialName("disconnected") class Disconnected: NetworkStatus()
@Serializable @SerialName("error") class Error(val error: String): NetworkStatus()
}
companion object { companion object {
val sampleData = Chat( val sampleData = Chat(
chatInfo = ChatInfo.Direct.sampleData, chatInfo = ChatInfo.Direct.sampleData,
@@ -605,6 +682,27 @@ sealed class ChatInfo: SomeChat, NamedChat {
} }
} }
@Serializable
sealed class NetworkStatus {
val statusString: String get() =
when (this) {
is Connected -> generalGetString(R.string.server_connected)
is Error -> generalGetString(R.string.server_error)
else -> generalGetString(R.string.server_connecting)
}
val statusExplanation: String get() =
when (this) {
is Connected -> generalGetString(R.string.connected_to_server_to_receive_messages_from_contact)
is Error -> String.format(generalGetString(R.string.trying_to_connect_to_server_to_receive_messages_with_error), error)
else -> generalGetString(R.string.trying_to_connect_to_server_to_receive_messages)
}
@Serializable @SerialName("unknown") class Unknown: NetworkStatus()
@Serializable @SerialName("connected") class Connected: NetworkStatus()
@Serializable @SerialName("disconnected") class Disconnected: NetworkStatus()
@Serializable @SerialName("error") class Error(val error: String): NetworkStatus()
}
@Serializable @Serializable
data class Contact( data class Contact(
val contactId: Long, val contactId: Long,
@@ -630,6 +728,7 @@ data class Contact(
ChatFeature.TimedMessages -> mergedPreferences.timedMessages.enabled.forUser ChatFeature.TimedMessages -> mergedPreferences.timedMessages.enabled.forUser
ChatFeature.FullDelete -> mergedPreferences.fullDelete.enabled.forUser ChatFeature.FullDelete -> mergedPreferences.fullDelete.enabled.forUser
ChatFeature.Voice -> mergedPreferences.voice.enabled.forUser ChatFeature.Voice -> mergedPreferences.voice.enabled.forUser
ChatFeature.Calls -> mergedPreferences.calls.enabled.forUser
} }
override val timedMessagesTTL: Int? get() = with(mergedPreferences.timedMessages) { if (enabled.forUser) userPreference.pref.ttl else null } override val timedMessagesTTL: Int? get() = with(mergedPreferences.timedMessages) { if (enabled.forUser) userPreference.pref.ttl else null }
override val displayName get() = localAlias.ifEmpty { profile.displayName } override val displayName get() = localAlias.ifEmpty { profile.displayName }
@@ -648,12 +747,14 @@ data class Contact(
ChatFeature.TimedMessages -> mergedPreferences.timedMessages.contactPreference.allow != FeatureAllowed.NO ChatFeature.TimedMessages -> mergedPreferences.timedMessages.contactPreference.allow != FeatureAllowed.NO
ChatFeature.FullDelete -> mergedPreferences.fullDelete.contactPreference.allow != FeatureAllowed.NO ChatFeature.FullDelete -> mergedPreferences.fullDelete.contactPreference.allow != FeatureAllowed.NO
ChatFeature.Voice -> mergedPreferences.voice.contactPreference.allow != FeatureAllowed.NO ChatFeature.Voice -> mergedPreferences.voice.contactPreference.allow != FeatureAllowed.NO
ChatFeature.Calls -> mergedPreferences.calls.contactPreference.allow != FeatureAllowed.NO
} }
fun userAllowsFeature(feature: ChatFeature): Boolean = when (feature) { fun userAllowsFeature(feature: ChatFeature): Boolean = when (feature) {
ChatFeature.TimedMessages -> mergedPreferences.timedMessages.userPreference.pref.allow != FeatureAllowed.NO ChatFeature.TimedMessages -> mergedPreferences.timedMessages.userPreference.pref.allow != FeatureAllowed.NO
ChatFeature.FullDelete -> mergedPreferences.fullDelete.userPreference.pref.allow != FeatureAllowed.NO ChatFeature.FullDelete -> mergedPreferences.fullDelete.userPreference.pref.allow != FeatureAllowed.NO
ChatFeature.Voice -> mergedPreferences.voice.userPreference.pref.allow != FeatureAllowed.NO ChatFeature.Voice -> mergedPreferences.voice.userPreference.pref.allow != FeatureAllowed.NO
ChatFeature.Calls -> mergedPreferences.calls.userPreference.pref.allow != FeatureAllowed.NO
} }
companion object { companion object {
@@ -675,6 +776,8 @@ data class Contact(
@Serializable @Serializable
class ContactRef( class ContactRef(
val contactId: Long, val contactId: Long,
val agentConnId: String,
val connId: Long,
var localDisplayName: String var localDisplayName: String
) { ) {
val id: ChatId get() = "@$contactId" val id: ChatId get() = "@$contactId"
@@ -689,6 +792,7 @@ class ContactSubStatus(
@Serializable @Serializable
data class Connection( data class Connection(
val connId: Long, val connId: Long,
val agentConnId: String,
val connStatus: ConnStatus, val connStatus: ConnStatus,
val connLevel: Int, val connLevel: Int,
val viaGroupLink: Boolean, val viaGroupLink: Boolean,
@@ -697,7 +801,7 @@ data class Connection(
) { ) {
val id: ChatId get() = ":$connId" val id: ChatId get() = ":$connId"
companion object { companion object {
val sampleData = Connection(connId = 1, connStatus = ConnStatus.Ready, connLevel = 0, viaGroupLink = false, customUserProfileId = null) val sampleData = Connection(connId = 1, agentConnId = "abc", connStatus = ConnStatus.Ready, connLevel = 0, viaGroupLink = false, customUserProfileId = null)
} }
} }
@@ -780,6 +884,7 @@ data class GroupInfo (
ChatFeature.TimedMessages -> fullGroupPreferences.timedMessages.on ChatFeature.TimedMessages -> fullGroupPreferences.timedMessages.on
ChatFeature.FullDelete -> fullGroupPreferences.fullDelete.on ChatFeature.FullDelete -> fullGroupPreferences.fullDelete.on
ChatFeature.Voice -> fullGroupPreferences.voice.on ChatFeature.Voice -> fullGroupPreferences.voice.on
ChatFeature.Calls -> false
} }
override val timedMessagesTTL: Int? get() = with(fullGroupPreferences.timedMessages) { if (on) ttl else null } override val timedMessagesTTL: Int? get() = with(fullGroupPreferences.timedMessages) { if (on) ttl else null }
override val displayName get() = groupProfile.displayName override val displayName get() = groupProfile.displayName
@@ -920,11 +1025,13 @@ class GroupMemberRef(
@Serializable @Serializable
enum class GroupMemberRole(val memberRole: String) { enum class GroupMemberRole(val memberRole: String) {
@SerialName("member") Member("member"), // order matters in comparisons @SerialName("observer") Observer("observer"), // order matters in comparisons
@SerialName("member") Member("member"),
@SerialName("admin") Admin("admin"), @SerialName("admin") Admin("admin"),
@SerialName("owner") Owner("owner"); @SerialName("owner") Owner("owner");
val text: String get() = when (this) { val text: String get() = when (this) {
Observer -> generalGetString(R.string.group_member_role_observer)
Member -> generalGetString(R.string.group_member_role_member) Member -> generalGetString(R.string.group_member_role_member)
Admin -> generalGetString(R.string.group_member_role_admin) Admin -> generalGetString(R.string.group_member_role_admin)
Owner -> generalGetString(R.string.group_member_role_owner) Owner -> generalGetString(R.string.group_member_role_owner)
@@ -1175,9 +1282,24 @@ data class ChatItem (
when (content) { when (content) {
is CIContent.SndDeleted -> true is CIContent.SndDeleted -> true
is CIContent.RcvDeleted -> true is CIContent.RcvDeleted -> true
is CIContent.SndModerated -> true
is CIContent.RcvModerated -> true
else -> false else -> false
} }
fun memberToModerate(chatInfo: ChatInfo): Pair<GroupInfo, GroupMember>? {
return if (chatInfo is ChatInfo.Group && chatDir is CIDirection.GroupRcv) {
val m = chatInfo.groupInfo.membership
if (m.memberRole >= GroupMemberRole.Admin && m.memberRole >= chatDir.groupMember.memberRole && meta.itemDeleted == null) {
chatInfo.groupInfo to chatDir.groupMember
} else {
null
}
} else {
null
}
}
private val showNtfDir: Boolean get() = !chatDir.sent private val showNtfDir: Boolean get() = !chatDir.sent
val showNotification: Boolean get() = val showNotification: Boolean get() =
@@ -1189,6 +1311,7 @@ data class ChatItem (
is CIContent.SndCall -> showNtfDir is CIContent.SndCall -> showNtfDir
is CIContent.RcvCall -> false // notification is shown on CallInvitation instead is CIContent.RcvCall -> false // notification is shown on CallInvitation instead
is CIContent.RcvIntegrityError -> showNtfDir is CIContent.RcvIntegrityError -> showNtfDir
is CIContent.RcvDecryptionError -> showNtfDir
is CIContent.RcvGroupInvitation -> showNtfDir is CIContent.RcvGroupInvitation -> showNtfDir
is CIContent.SndGroupInvitation -> showNtfDir is CIContent.SndGroupInvitation -> showNtfDir
is CIContent.RcvGroupEventContent -> when (content.rcvGroupEvent) { is CIContent.RcvGroupEventContent -> when (content.rcvGroupEvent) {
@@ -1214,6 +1337,8 @@ data class ChatItem (
is CIContent.SndGroupFeature -> showNtfDir is CIContent.SndGroupFeature -> showNtfDir
is CIContent.RcvChatFeatureRejected -> showNtfDir is CIContent.RcvChatFeatureRejected -> showNtfDir
is CIContent.RcvGroupFeatureRejected -> showNtfDir is CIContent.RcvGroupFeatureRejected -> showNtfDir
is CIContent.SndModerated -> true
is CIContent.RcvModerated -> true
is CIContent.InvalidJSON -> false is CIContent.InvalidJSON -> false
} }
@@ -1228,14 +1353,14 @@ data class ChatItem (
status: CIStatus = CIStatus.SndNew(), status: CIStatus = CIStatus.SndNew(),
quotedItem: CIQuote? = null, quotedItem: CIQuote? = null,
file: CIFile? = null, file: CIFile? = null,
itemDeleted: Boolean = false, itemDeleted: CIDeleted? = null,
itemEdited: Boolean = false, itemEdited: Boolean = false,
itemTimed: CITimed? = null, itemTimed: CITimed? = null,
editable: Boolean = true editable: Boolean = true
) = ) =
ChatItem( ChatItem(
chatDir = dir, chatDir = dir,
meta = CIMeta.getSample(id, ts, text, status, itemDeleted, itemEdited, null, editable), meta = CIMeta.getSample(id, ts, text, status, itemDeleted, itemEdited, itemTimed, editable),
content = CIContent.SndMsgContent(msgContent = MsgContent.MCText(text)), content = CIContent.SndMsgContent(msgContent = MsgContent.MCText(text)),
quotedItem = quotedItem, quotedItem = quotedItem,
file = file file = file
@@ -1250,7 +1375,7 @@ data class ChatItem (
) = ) =
ChatItem( ChatItem(
chatDir = CIDirection.DirectRcv(), chatDir = CIDirection.DirectRcv(),
meta = CIMeta.getSample(id, Clock.System.now(), text, CIStatus.RcvRead(), itemDeleted = false, itemEdited = false, editable = false), meta = CIMeta.getSample(id, Clock.System.now(), text, CIStatus.RcvRead()),
content = CIContent.RcvMsgContent(msgContent = MsgContent.MCFile(text)), content = CIContent.RcvMsgContent(msgContent = MsgContent.MCFile(text)),
quotedItem = null, quotedItem = null,
file = CIFile.getSample(fileName = fileName, fileSize = fileSize, fileStatus = fileStatus) file = CIFile.getSample(fileName = fileName, fileSize = fileSize, fileStatus = fileStatus)
@@ -1265,7 +1390,7 @@ data class ChatItem (
) = ) =
ChatItem( ChatItem(
chatDir = dir, chatDir = dir,
meta = CIMeta.getSample(id, ts, text, status, itemDeleted = false, itemEdited = false, editable = false), meta = CIMeta.getSample(id, ts, text, status),
content = CIContent.RcvDeleted(deleteMode = CIDeleteMode.cidmBroadcast), content = CIContent.RcvDeleted(deleteMode = CIDeleteMode.cidmBroadcast),
quotedItem = null, quotedItem = null,
file = null file = null
@@ -1274,7 +1399,7 @@ data class ChatItem (
fun getGroupInvitationSample(status: CIGroupInvitationStatus = CIGroupInvitationStatus.Pending) = fun getGroupInvitationSample(status: CIGroupInvitationStatus = CIGroupInvitationStatus.Pending) =
ChatItem( ChatItem(
chatDir = CIDirection.DirectRcv(), chatDir = CIDirection.DirectRcv(),
meta = CIMeta.getSample(1, Clock.System.now(), "received invitation to join group team as admin", CIStatus.RcvRead(), itemDeleted = false, itemEdited = false, editable = false), meta = CIMeta.getSample(1, Clock.System.now(), "received invitation to join group team as admin", CIStatus.RcvRead()),
content = CIContent.RcvGroupInvitation(groupInvitation = CIGroupInvitation.getSample(status = status), memberRole = GroupMemberRole.Admin), content = CIContent.RcvGroupInvitation(groupInvitation = CIGroupInvitation.getSample(status = status), memberRole = GroupMemberRole.Admin),
quotedItem = null, quotedItem = null,
file = null file = null
@@ -1283,7 +1408,7 @@ data class ChatItem (
fun getGroupEventSample() = fun getGroupEventSample() =
ChatItem( ChatItem(
chatDir = CIDirection.DirectRcv(), chatDir = CIDirection.DirectRcv(),
meta = CIMeta.getSample(1, Clock.System.now(), "group event text", CIStatus.RcvRead(), itemDeleted = false, itemEdited = false, editable = false), meta = CIMeta.getSample(1, Clock.System.now(), "group event text", CIStatus.RcvRead()),
content = CIContent.RcvGroupEventContent(rcvGroupEvent = RcvGroupEvent.MemberAdded(groupMemberId = 1, profile = Profile.sampleData)), content = CIContent.RcvGroupEventContent(rcvGroupEvent = RcvGroupEvent.MemberAdded(groupMemberId = 1, profile = Profile.sampleData)),
quotedItem = null, quotedItem = null,
file = null file = null
@@ -1293,13 +1418,13 @@ data class ChatItem (
val content = CIContent.RcvChatFeature(feature = feature, enabled = enabled, param = null) val content = CIContent.RcvChatFeature(feature = feature, enabled = enabled, param = null)
return ChatItem( return ChatItem(
chatDir = CIDirection.DirectRcv(), chatDir = CIDirection.DirectRcv(),
meta = CIMeta.getSample(1, Clock.System.now(), content.text, CIStatus.RcvRead(), itemDeleted = false, itemEdited = false, editable = false), meta = CIMeta.getSample(1, Clock.System.now(), content.text, CIStatus.RcvRead()),
content = content, content = content,
quotedItem = null, quotedItem = null,
file = null file = null
) )
} }
private const val TEMP_DELETED_CHAT_ITEM_ID = -1L private const val TEMP_DELETED_CHAT_ITEM_ID = -1L
const val TEMP_LIVE_CHAT_ITEM_ID = -2L const val TEMP_LIVE_CHAT_ITEM_ID = -2L
@@ -1313,7 +1438,7 @@ data class ChatItem (
itemStatus = CIStatus.RcvRead(), itemStatus = CIStatus.RcvRead(),
createdAt = Clock.System.now(), createdAt = Clock.System.now(),
updatedAt = Clock.System.now(), updatedAt = Clock.System.now(),
itemDeleted = false, itemDeleted = null,
itemEdited = false, itemEdited = false,
itemTimed = null, itemTimed = null,
itemLive = false, itemLive = false,
@@ -1333,7 +1458,7 @@ data class ChatItem (
itemStatus = CIStatus.RcvRead(), itemStatus = CIStatus.RcvRead(),
createdAt = Clock.System.now(), createdAt = Clock.System.now(),
updatedAt = Clock.System.now(), updatedAt = Clock.System.now(),
itemDeleted = false, itemDeleted = null,
itemEdited = false, itemEdited = false,
itemTimed = null, itemTimed = null,
itemLive = true, itemLive = true,
@@ -1344,10 +1469,10 @@ data class ChatItem (
file = null file = null
) )
fun invalidJSON(json: String): ChatItem = fun invalidJSON(chatDir: CIDirection?, meta: CIMeta?, json: String): ChatItem =
ChatItem( ChatItem(
chatDir = CIDirection.DirectSnd(), chatDir = chatDir ?: CIDirection.DirectSnd(),
meta = CIMeta.invalidJSON(), meta = meta ?: CIMeta.invalidJSON(),
content = CIContent.InvalidJSON(json), content = CIContent.InvalidJSON(json),
quotedItem = null, quotedItem = null,
file = null file = null
@@ -1378,7 +1503,7 @@ data class CIMeta (
val itemStatus: CIStatus, val itemStatus: CIStatus,
val createdAt: Instant, val createdAt: Instant,
val updatedAt: Instant, val updatedAt: Instant,
val itemDeleted: Boolean, val itemDeleted: CIDeleted?,
val itemEdited: Boolean, val itemEdited: Boolean,
val itemTimed: CITimed?, val itemTimed: CITimed?,
val itemLive: Boolean?, val itemLive: Boolean?,
@@ -1391,19 +1516,19 @@ data class CIMeta (
val isRcvNew: Boolean get() = itemStatus is CIStatus.RcvNew val isRcvNew: Boolean get() = itemStatus is CIStatus.RcvNew
fun statusIcon(primaryColor: Color, metaColor: Color = HighOrLowlight): Pair<ImageVector, Color>? = fun statusIcon(primaryColor: Color, metaColor: Color = CurrentColors.value.colors.secondary): Pair<Int, Color>? =
when (itemStatus) { when (itemStatus) {
is CIStatus.SndSent -> Icons.Filled.Check to metaColor is CIStatus.SndSent -> R.drawable.ic_check_filled to metaColor
is CIStatus.SndErrorAuth -> Icons.Filled.Close to Color.Red is CIStatus.SndErrorAuth -> R.drawable.ic_close to Color.Red
is CIStatus.SndError -> Icons.Filled.WarningAmber to WarningYellow is CIStatus.SndError -> R.drawable.ic_warning_filled to WarningYellow
is CIStatus.RcvNew -> Icons.Filled.Circle to primaryColor is CIStatus.RcvNew -> R.drawable.ic_circle_filled to primaryColor
else -> null else -> null
} }
companion object { companion object {
fun getSample( fun getSample(
id: Long, ts: Instant, text: String, status: CIStatus = CIStatus.SndNew(), id: Long, ts: Instant, text: String, status: CIStatus = CIStatus.SndNew(),
itemDeleted: Boolean = false, itemEdited: Boolean = false, itemTimed: CITimed? = null, itemLive: Boolean = false, editable: Boolean = true itemDeleted: CIDeleted? = null, itemEdited: Boolean = false, itemTimed: CITimed? = null, itemLive: Boolean = false, editable: Boolean = true
): CIMeta = ): CIMeta =
CIMeta( CIMeta(
itemId = id, itemId = id,
@@ -1428,7 +1553,7 @@ data class CIMeta (
itemStatus = CIStatus.SndNew(), itemStatus = CIStatus.SndNew(),
createdAt = Clock.System.now(), createdAt = Clock.System.now(),
updatedAt = Clock.System.now(), updatedAt = Clock.System.now(),
itemDeleted = false, itemDeleted = null,
itemEdited = false, itemEdited = false,
itemTimed = null, itemTimed = null,
itemLive = false, itemLive = false,
@@ -1463,6 +1588,12 @@ sealed class CIStatus {
@Serializable @SerialName("rcvRead") class RcvRead: CIStatus() @Serializable @SerialName("rcvRead") class RcvRead: CIStatus()
} }
@Serializable
sealed class CIDeleted {
@Serializable @SerialName("deleted") class Deleted: CIDeleted()
@Serializable @SerialName("moderated") class Moderated(val byGroupMember: GroupMember): CIDeleted()
}
@Serializable @Serializable
enum class CIDeleteMode(val deleteMode: String) { enum class CIDeleteMode(val deleteMode: String) {
@SerialName("internal") cidmInternal("internal"), @SerialName("internal") cidmInternal("internal"),
@@ -1484,6 +1615,7 @@ sealed class CIContent: ItemContent {
@Serializable @SerialName("sndCall") class SndCall(val status: CICallStatus, val duration: Int): CIContent() { override val msgContent: MsgContent? get() = null } @Serializable @SerialName("sndCall") class SndCall(val status: CICallStatus, val duration: Int): CIContent() { override val msgContent: MsgContent? get() = null }
@Serializable @SerialName("rcvCall") class RcvCall(val status: CICallStatus, val duration: Int): CIContent() { override val msgContent: MsgContent? get() = null } @Serializable @SerialName("rcvCall") class RcvCall(val status: CICallStatus, val duration: Int): CIContent() { override val msgContent: MsgContent? get() = null }
@Serializable @SerialName("rcvIntegrityError") class RcvIntegrityError(val msgError: MsgErrorType): CIContent() { override val msgContent: MsgContent? get() = null } @Serializable @SerialName("rcvIntegrityError") class RcvIntegrityError(val msgError: MsgErrorType): CIContent() { override val msgContent: MsgContent? get() = null }
@Serializable @SerialName("rcvDecryptionError") class RcvDecryptionError(val msgDecryptError: MsgDecryptError, val msgCount: UInt): CIContent() { override val msgContent: MsgContent? get() = null }
@Serializable @SerialName("rcvGroupInvitation") class RcvGroupInvitation(val groupInvitation: CIGroupInvitation, val memberRole: GroupMemberRole): CIContent() { override val msgContent: MsgContent? get() = null } @Serializable @SerialName("rcvGroupInvitation") class RcvGroupInvitation(val groupInvitation: CIGroupInvitation, val memberRole: GroupMemberRole): CIContent() { override val msgContent: MsgContent? get() = null }
@Serializable @SerialName("sndGroupInvitation") class SndGroupInvitation(val groupInvitation: CIGroupInvitation, val memberRole: GroupMemberRole): CIContent() { override val msgContent: MsgContent? get() = null } @Serializable @SerialName("sndGroupInvitation") class SndGroupInvitation(val groupInvitation: CIGroupInvitation, val memberRole: GroupMemberRole): CIContent() { override val msgContent: MsgContent? get() = null }
@Serializable @SerialName("rcvGroupEvent") class RcvGroupEventContent(val rcvGroupEvent: RcvGroupEvent): CIContent() { override val msgContent: MsgContent? get() = null } @Serializable @SerialName("rcvGroupEvent") class RcvGroupEventContent(val rcvGroupEvent: RcvGroupEvent): CIContent() { override val msgContent: MsgContent? get() = null }
@@ -1498,6 +1630,8 @@ sealed class CIContent: ItemContent {
@Serializable @SerialName("sndGroupFeature") class SndGroupFeature(val groupFeature: GroupFeature, val preference: GroupPreference, val param: Int? = null): CIContent() { override val msgContent: MsgContent? get() = null } @Serializable @SerialName("sndGroupFeature") class SndGroupFeature(val groupFeature: GroupFeature, val preference: GroupPreference, val param: Int? = null): CIContent() { override val msgContent: MsgContent? get() = null }
@Serializable @SerialName("rcvChatFeatureRejected") class RcvChatFeatureRejected(val feature: ChatFeature): CIContent() { override val msgContent: MsgContent? get() = null } @Serializable @SerialName("rcvChatFeatureRejected") class RcvChatFeatureRejected(val feature: ChatFeature): CIContent() { override val msgContent: MsgContent? get() = null }
@Serializable @SerialName("rcvGroupFeatureRejected") class RcvGroupFeatureRejected(val groupFeature: GroupFeature): CIContent() { override val msgContent: MsgContent? get() = null } @Serializable @SerialName("rcvGroupFeatureRejected") class RcvGroupFeatureRejected(val groupFeature: GroupFeature): CIContent() { override val msgContent: MsgContent? get() = null }
@Serializable @SerialName("sndModerated") object SndModerated: CIContent() { override val msgContent: MsgContent? get() = null }
@Serializable @SerialName("rcvModerated") object RcvModerated: CIContent() { override val msgContent: MsgContent? get() = null }
@Serializable @SerialName("invalidJSON") data class InvalidJSON(val json: String): CIContent() { override val msgContent: MsgContent? get() = null } @Serializable @SerialName("invalidJSON") data class InvalidJSON(val json: String): CIContent() { override val msgContent: MsgContent? get() = null }
override val text: String get() = when (this) { override val text: String get() = when (this) {
@@ -1508,6 +1642,7 @@ sealed class CIContent: ItemContent {
is SndCall -> status.text(duration) is SndCall -> status.text(duration)
is RcvCall -> status.text(duration) is RcvCall -> status.text(duration)
is RcvIntegrityError -> msgError.text is RcvIntegrityError -> msgError.text
is RcvDecryptionError -> msgDecryptError.text
is RcvGroupInvitation -> groupInvitation.text is RcvGroupInvitation -> groupInvitation.text
is SndGroupInvitation -> groupInvitation.text is SndGroupInvitation -> groupInvitation.text
is RcvGroupEventContent -> rcvGroupEvent.text is RcvGroupEventContent -> rcvGroupEvent.text
@@ -1522,6 +1657,8 @@ sealed class CIContent: ItemContent {
is SndGroupFeature -> featureText(groupFeature, preference.enable.text, param) is SndGroupFeature -> featureText(groupFeature, preference.enable.text, param)
is RcvChatFeatureRejected -> "${feature.text}: ${generalGetString(R.string.feature_received_prohibited)}" is RcvChatFeatureRejected -> "${feature.text}: ${generalGetString(R.string.feature_received_prohibited)}"
is RcvGroupFeatureRejected -> "${groupFeature.text}: ${generalGetString(R.string.feature_received_prohibited)}" is RcvGroupFeatureRejected -> "${groupFeature.text}: ${generalGetString(R.string.feature_received_prohibited)}"
is SndModerated -> generalGetString(R.string.moderated_description)
is RcvModerated -> generalGetString(R.string.moderated_description)
is InvalidJSON -> "invalid data" is InvalidJSON -> "invalid data"
} }
@@ -1535,15 +1672,26 @@ sealed class CIContent: ItemContent {
fun preferenceText(feature: Feature, allowed: FeatureAllowed, param: Int?): String = when { fun preferenceText(feature: Feature, allowed: FeatureAllowed, param: Int?): String = when {
allowed != FeatureAllowed.NO && feature.hasParam && param != null -> allowed != FeatureAllowed.NO && feature.hasParam && param != null ->
"offered ${feature.text}: ${TimedMessagesPreference.ttlText(param)}" String.format(generalGetString(R.string.feature_offered_item_with_param), feature.text, TimedMessagesPreference.ttlText(param))
allowed != FeatureAllowed.NO -> allowed != FeatureAllowed.NO ->
"offered ${feature.text}" String.format(generalGetString(R.string.feature_offered_item), feature.text, TimedMessagesPreference.ttlText(param))
else -> else ->
"cancelled ${feature.text}" String.format(generalGetString(R.string.feature_cancelled_item), feature.text, TimedMessagesPreference.ttlText(param))
} }
} }
} }
@Serializable
enum class MsgDecryptError {
@SerialName("ratchetHeader") RatchetHeader,
@SerialName("tooManySkipped") TooManySkipped;
val text: String get() = when (this) {
RatchetHeader -> generalGetString(R.string.decryption_error)
TooManySkipped -> generalGetString(R.string.decryption_error)
}
}
@Serializable @Serializable
class CIQuote ( class CIQuote (
val chatDir: CIDirection? = null, val chatDir: CIDirection? = null,
@@ -1581,18 +1729,40 @@ class CIFile(
val fileName: String, val fileName: String,
val fileSize: Long, val fileSize: Long,
val filePath: String? = null, val filePath: String? = null,
val fileStatus: CIFileStatus val fileStatus: CIFileStatus,
val fileProtocol: FileProtocol
) { ) {
val loaded: Boolean = when (fileStatus) { val loaded: Boolean = when (fileStatus) {
CIFileStatus.SndStored -> true is CIFileStatus.SndStored -> true
CIFileStatus.SndTransfer -> true is CIFileStatus.SndTransfer -> true
CIFileStatus.SndComplete -> true is CIFileStatus.SndComplete -> true
CIFileStatus.SndCancelled -> true is CIFileStatus.SndCancelled -> true
CIFileStatus.RcvInvitation -> false is CIFileStatus.SndError -> true
CIFileStatus.RcvAccepted -> false is CIFileStatus.RcvInvitation -> false
CIFileStatus.RcvTransfer -> false is CIFileStatus.RcvAccepted -> false
CIFileStatus.RcvCancelled -> false is CIFileStatus.RcvTransfer -> false
CIFileStatus.RcvComplete -> true is CIFileStatus.RcvCancelled -> false
is CIFileStatus.RcvComplete -> true
is CIFileStatus.RcvError -> false
}
val cancelAction: CancelAction? = when (fileStatus) {
is CIFileStatus.SndStored -> sndCancelAction
is CIFileStatus.SndTransfer -> sndCancelAction
is CIFileStatus.SndComplete ->
if (fileProtocol == FileProtocol.XFTP) {
revokeCancelAction
} else {
null
}
is CIFileStatus.SndCancelled -> null
is CIFileStatus.SndError -> null
is CIFileStatus.RcvInvitation -> null
is CIFileStatus.RcvAccepted -> rcvCancelAction
is CIFileStatus.RcvTransfer -> rcvCancelAction
is CIFileStatus.RcvCancelled -> null
is CIFileStatus.RcvComplete -> null
is CIFileStatus.RcvError -> null
} }
companion object { companion object {
@@ -1603,21 +1773,67 @@ class CIFile(
filePath: String? = "test.txt", filePath: String? = "test.txt",
fileStatus: CIFileStatus = CIFileStatus.RcvComplete fileStatus: CIFileStatus = CIFileStatus.RcvComplete
): CIFile = ): CIFile =
CIFile(fileId = fileId, fileName = fileName, fileSize = fileSize, filePath = filePath, fileStatus = fileStatus) CIFile(fileId = fileId, fileName = fileName, fileSize = fileSize, filePath = filePath, fileStatus = fileStatus, fileProtocol = FileProtocol.XFTP)
} }
} }
@Serializable @Serializable
enum class CIFileStatus { class CancelAction(
@SerialName("snd_stored") SndStored, val uiActionId: Int,
@SerialName("snd_transfer") SndTransfer, val alert: AlertInfo
@SerialName("snd_complete") SndComplete, )
@SerialName("snd_cancelled") SndCancelled,
@SerialName("rcv_invitation") RcvInvitation, @Serializable
@SerialName("rcv_accepted") RcvAccepted, class AlertInfo(
@SerialName("rcv_transfer") RcvTransfer, val titleId: Int,
@SerialName("rcv_complete") RcvComplete, val messageId: Int,
@SerialName("rcv_cancelled") RcvCancelled; val confirmId: Int
)
private val sndCancelAction: CancelAction = CancelAction(
uiActionId = R.string.stop_file__action,
alert = AlertInfo(
titleId = R.string.stop_snd_file__title,
messageId = R.string.stop_snd_file__message,
confirmId = R.string.stop_file__confirm
)
)
private val revokeCancelAction: CancelAction = CancelAction(
uiActionId = R.string.revoke_file__action,
alert = AlertInfo(
titleId = R.string.revoke_file__title,
messageId = R.string.revoke_file__message,
confirmId = R.string.revoke_file__confirm
)
)
private val rcvCancelAction: CancelAction = CancelAction(
uiActionId = R.string.stop_file__action,
alert = AlertInfo(
titleId = R.string.stop_rcv_file__title,
messageId = R.string.stop_rcv_file__message,
confirmId = R.string.stop_file__confirm
)
)
@Serializable
enum class FileProtocol {
@SerialName("smp") SMP,
@SerialName("xftp") XFTP;
}
@Serializable
sealed class CIFileStatus {
@Serializable @SerialName("sndStored") object SndStored: CIFileStatus()
@Serializable @SerialName("sndTransfer") class SndTransfer(val sndProgress: Long, val sndTotal: Long): CIFileStatus()
@Serializable @SerialName("sndComplete") object SndComplete: CIFileStatus()
@Serializable @SerialName("sndCancelled") object SndCancelled: CIFileStatus()
@Serializable @SerialName("sndError") object SndError: CIFileStatus()
@Serializable @SerialName("rcvInvitation") object RcvInvitation: CIFileStatus()
@Serializable @SerialName("rcvAccepted") object RcvAccepted: CIFileStatus()
@Serializable @SerialName("rcvTransfer") class RcvTransfer(val rcvProgress: Long, val rcvTotal: Long): CIFileStatus()
@Serializable @SerialName("rcvComplete") object RcvComplete: CIFileStatus()
@Serializable @SerialName("rcvCancelled") object RcvCancelled: CIFileStatus()
@Serializable @SerialName("rcvError") object RcvError: CIFileStatus()
} }
@Suppress("SERIALIZER_TYPE_INCOMPATIBLE") @Suppress("SERIALIZER_TYPE_INCOMPATIBLE")
@@ -1628,6 +1844,7 @@ sealed class MsgContent {
@Serializable(with = MsgContentSerializer::class) class MCText(override val text: String): MsgContent() @Serializable(with = MsgContentSerializer::class) class MCText(override val text: String): MsgContent()
@Serializable(with = MsgContentSerializer::class) class MCLink(override val text: String, val preview: LinkPreview): MsgContent() @Serializable(with = MsgContentSerializer::class) class MCLink(override val text: String, val preview: LinkPreview): MsgContent()
@Serializable(with = MsgContentSerializer::class) class MCImage(override val text: String, val image: String): MsgContent() @Serializable(with = MsgContentSerializer::class) class MCImage(override val text: String, val image: String): MsgContent()
@Serializable(with = MsgContentSerializer::class) class MCVideo(override val text: String, val image: String, val duration: Int): MsgContent()
@Serializable(with = MsgContentSerializer::class) class MCVoice(override val text: String, val duration: Int): MsgContent() @Serializable(with = MsgContentSerializer::class) class MCVoice(override val text: String, val duration: Int): MsgContent()
@Serializable(with = MsgContentSerializer::class) class MCFile(override val text: String): MsgContent() @Serializable(with = MsgContentSerializer::class) class MCFile(override val text: String): MsgContent()
@Serializable(with = MsgContentSerializer::class) class MCUnknown(val type: String? = null, override val text: String, val json: JsonElement): MsgContent() @Serializable(with = MsgContentSerializer::class) class MCUnknown(val type: String? = null, override val text: String, val json: JsonElement): MsgContent()
@@ -1681,6 +1898,11 @@ object MsgContentSerializer : KSerializer<MsgContent> {
element<String>("text") element<String>("text")
element<String>("image") element<String>("image")
}) })
element("MCVideo", buildClassSerialDescriptor("MCVideo") {
element<String>("text")
element<String>("image")
element<Int>("duration")
})
element("MCFile", buildClassSerialDescriptor("MCFile") { element("MCFile", buildClassSerialDescriptor("MCFile") {
element<String>("text") element<String>("text")
}) })
@@ -1704,6 +1926,11 @@ object MsgContentSerializer : KSerializer<MsgContent> {
val image = json["image"]?.jsonPrimitive?.content ?: "unknown message format" val image = json["image"]?.jsonPrimitive?.content ?: "unknown message format"
MsgContent.MCImage(text, image) MsgContent.MCImage(text, image)
} }
"video" -> {
val image = json["image"]?.jsonPrimitive?.content ?: "unknown message format"
val duration = json["duration"]?.jsonPrimitive?.intOrNull ?: 0
MsgContent.MCVideo(text, image, duration)
}
"voice" -> { "voice" -> {
val duration = json["duration"]?.jsonPrimitive?.intOrNull ?: 0 val duration = json["duration"]?.jsonPrimitive?.intOrNull ?: 0
MsgContent.MCVoice(text, duration) MsgContent.MCVoice(text, duration)
@@ -1739,6 +1966,13 @@ object MsgContentSerializer : KSerializer<MsgContent> {
put("text", value.text) put("text", value.text)
put("image", value.image) put("image", value.image)
} }
is MsgContent.MCVideo ->
buildJsonObject {
put("type", "video")
put("text", value.text)
put("image", value.image)
put("duration", value.duration)
}
is MsgContent.MCVoice -> is MsgContent.MCVoice ->
buildJsonObject { buildJsonObject {
put("type", "voice") put("type", "voice")
@@ -1874,7 +2108,11 @@ enum class CICallStatus {
} }
} }
fun durationText(sec: Int): String = "%02d:%02d".format(sec / 60, sec % 60) fun durationText(sec: Int): String {
val s = sec % 60
val m = sec / 60
return if (m < 60) "%02d:%02d".format(m, s) else "%02d:%02d:%02d".format(m / 60, m % 60, s)
}
@Serializable @Serializable
sealed class MsgErrorType() { sealed class MsgErrorType() {
@@ -30,7 +30,13 @@ class NtfManager(val context: Context, private val appPreferences: AppPreference
const val RejectCallAction: String = "chat.simplex.app.REJECT_CALL" const val RejectCallAction: String = "chat.simplex.app.REJECT_CALL"
const val CallNotificationId: Int = -1 const val CallNotificationId: Int = -1
private const val UserIdKey: String = "userId"
private const val ChatIdKey: String = "chatId" private const val ChatIdKey: String = "chatId"
fun getUserIdFromIntent(intent: Intent?): Long? {
val userId = intent?.getLongExtra(UserIdKey, -1L)
return if (userId == -1L || userId == null) null else userId
}
} }
private val manager: NotificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager private val manager: NotificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
@@ -38,11 +44,7 @@ class NtfManager(val context: Context, private val appPreferences: AppPreference
private val msgNtfTimeoutMs = 30000L private val msgNtfTimeoutMs = 30000L
init { init {
manager.createNotificationChannel(NotificationChannel(MessageChannel, generalGetString(R.string.ntf_channel_messages), NotificationManager.IMPORTANCE_HIGH)) if (manager.areNotificationsEnabled()) createNtfChannelsMaybeShowAlert()
manager.createNotificationChannel(callNotificationChannel(CallChannel, generalGetString(R.string.ntf_channel_calls)))
// Remove old channels since they can't be edited
manager.deleteNotificationChannel("chat.simplex.app.CALL_NOTIFICATION")
manager.deleteNotificationChannel("chat.simplex.app.LOCK_SCREEN_CALL_NOTIFICATION")
} }
enum class NotificationAction { enum class NotificationAction {
@@ -77,8 +79,9 @@ class NtfManager(val context: Context, private val appPreferences: AppPreference
} }
} }
fun notifyContactRequestReceived(cInfo: ChatInfo.ContactRequest) { fun notifyContactRequestReceived(user: User, cInfo: ChatInfo.ContactRequest) {
notifyMessageReceived( displayNotification(
user = user,
chatId = cInfo.id, chatId = cInfo.id,
displayName = cInfo.displayName, displayName = cInfo.displayName,
msgText = generalGetString(R.string.notification_new_contact_request), msgText = generalGetString(R.string.notification_new_contact_request),
@@ -87,21 +90,22 @@ class NtfManager(val context: Context, private val appPreferences: AppPreference
) )
} }
fun notifyContactConnected(contact: Contact) { fun notifyContactConnected(user: User, contact: Contact) {
notifyMessageReceived( displayNotification(
user = user,
chatId = contact.id, chatId = contact.id,
displayName = contact.displayName, displayName = contact.displayName,
msgText = generalGetString(R.string.notification_contact_connected) msgText = generalGetString(R.string.notification_contact_connected)
) )
} }
fun notifyMessageReceived(cInfo: ChatInfo, cItem: ChatItem) { fun notifyMessageReceived(user: User, cInfo: ChatInfo, cItem: ChatItem) {
if (!cInfo.ntfsEnabled) return if (!cInfo.ntfsEnabled) return
displayNotification(user = user, chatId = cInfo.id, displayName = cInfo.displayName, msgText = hideSecrets(cItem))
notifyMessageReceived(chatId = cInfo.id, displayName = cInfo.displayName, msgText = hideSecrets(cItem))
} }
fun notifyMessageReceived(chatId: String, displayName: String, msgText: String, image: String? = null, actions: List<NotificationAction> = emptyList()) { fun displayNotification(user: User, chatId: String, displayName: String, msgText: String, image: String? = null, actions: List<NotificationAction> = emptyList()) {
if (!user.showNotifications) return
Log.d(TAG, "notifyMessageReceived $chatId") Log.d(TAG, "notifyMessageReceived $chatId")
val now = Clock.System.now().toEpochMilliseconds() val now = Clock.System.now().toEpochMilliseconds()
val recentNotification = (now - prevNtfTime.getOrDefault(chatId, 0) < msgNtfTimeoutMs) val recentNotification = (now - prevNtfTime.getOrDefault(chatId, 0) < msgNtfTimeoutMs)
@@ -126,13 +130,14 @@ class NtfManager(val context: Context, private val appPreferences: AppPreference
.setColor(0x88FFFF) .setColor(0x88FFFF)
.setAutoCancel(true) .setAutoCancel(true)
.setVibrate(if (actions.isEmpty()) null else longArrayOf(0, 250, 250, 250)) .setVibrate(if (actions.isEmpty()) null else longArrayOf(0, 250, 250, 250))
.setContentIntent(chatPendingIntent(OpenChatAction, chatId)) .setContentIntent(chatPendingIntent(OpenChatAction, user.userId, chatId))
.setSilent(if (actions.isEmpty()) recentNotification else false) .setSilent(if (actions.isEmpty()) recentNotification else false)
for (action in actions) { for (action in actions) {
val flags = PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE val flags = PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
val actionIntent = Intent(SimplexApp.context, NtfActionReceiver::class.java) val actionIntent = Intent(SimplexApp.context, NtfActionReceiver::class.java)
actionIntent.action = action.name actionIntent.action = action.name
actionIntent.putExtra(UserIdKey, user.userId)
actionIntent.putExtra(ChatIdKey, chatId) actionIntent.putExtra(ChatIdKey, chatId)
val actionPendingIntent: PendingIntent = PendingIntent.getBroadcast(SimplexApp.context, 0, actionIntent, flags) val actionPendingIntent: PendingIntent = PendingIntent.getBroadcast(SimplexApp.context, 0, actionIntent, flags)
val actionButton = when (action) { val actionButton = when (action) {
@@ -147,7 +152,7 @@ class NtfManager(val context: Context, private val appPreferences: AppPreference
.setGroup(MessageGroup) .setGroup(MessageGroup)
.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN) .setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN)
.setGroupSummary(true) .setGroupSummary(true)
.setContentIntent(chatPendingIntent(ShowChatsAction)) .setContentIntent(chatPendingIntent(ShowChatsAction, null))
.build() .build()
with(NotificationManagerCompat.from(context)) { with(NotificationManagerCompat.from(context)) {
@@ -182,9 +187,9 @@ class NtfManager(val context: Context, private val appPreferences: AppPreference
val soundUri = Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE + "://" + context.packageName + "/" + R.raw.ring_once) val soundUri = Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE + "://" + context.packageName + "/" + R.raw.ring_once)
val fullScreenPendingIntent = PendingIntent.getActivity(context, 0, Intent(), PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) val fullScreenPendingIntent = PendingIntent.getActivity(context, 0, Intent(), PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
NotificationCompat.Builder(context, CallChannel) NotificationCompat.Builder(context, CallChannel)
.setContentIntent(chatPendingIntent(OpenChatAction, invitation.contact.id)) .setContentIntent(chatPendingIntent(OpenChatAction, invitation.user.userId, invitation.contact.id))
.addAction(R.drawable.ntf_icon, generalGetString(R.string.accept), chatPendingIntent(AcceptCallAction, contactId)) .addAction(R.drawable.ntf_icon, generalGetString(R.string.accept), chatPendingIntent(AcceptCallAction, invitation.user.userId, contactId))
.addAction(R.drawable.ntf_icon, generalGetString(R.string.reject), chatPendingIntent(RejectCallAction, contactId, true)) .addAction(R.drawable.ntf_icon, generalGetString(R.string.reject), chatPendingIntent(RejectCallAction, invitation.user.userId, contactId, true))
.setFullScreenIntent(fullScreenPendingIntent, true) .setFullScreenIntent(fullScreenPendingIntent, true)
.setSound(soundUri) .setSound(soundUri)
} }
@@ -241,12 +246,13 @@ class NtfManager(val context: Context, private val appPreferences: AppPreference
} }
} }
private fun chatPendingIntent(intentAction: String, chatId: String? = null, broadcast: Boolean = false): PendingIntent { private fun chatPendingIntent(intentAction: String, userId: Long?, chatId: String? = null, broadcast: Boolean = false): PendingIntent {
Log.d(TAG, "chatPendingIntent for $intentAction") Log.d(TAG, "chatPendingIntent for $intentAction")
val uniqueInt = (System.currentTimeMillis() and 0xfffffff).toInt() val uniqueInt = (System.currentTimeMillis() and 0xfffffff).toInt()
var intent = Intent(context, if (!broadcast) MainActivity::class.java else NtfActionReceiver::class.java) var intent = Intent(context, if (!broadcast) MainActivity::class.java else NtfActionReceiver::class.java)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP) .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP)
.setAction(intentAction) .setAction(intentAction)
.putExtra(UserIdKey, userId)
if (chatId != null) intent = intent.putExtra(ChatIdKey, chatId) if (chatId != null) intent = intent.putExtra(ChatIdKey, chatId)
return if (!broadcast) { return if (!broadcast) {
TaskStackBuilder.create(context).run { TaskStackBuilder.create(context).run {
@@ -258,24 +264,46 @@ class NtfManager(val context: Context, private val appPreferences: AppPreference
} }
} }
/**
* This function creates notifications channels. On Android 13+ calling it for the first time will trigger system alert,
* The alert asks a user to allow or disallow to show notifications for the app. That's why it should be called only when the user
* already saw such alert or when you want to trigger showing the alert.
* On the first app launch the channels will be created after user profile is created. Subsequent calls will create new channels and delete
* old ones if needed
* */
fun createNtfChannelsMaybeShowAlert() {
manager.createNotificationChannel(NotificationChannel(MessageChannel, generalGetString(R.string.ntf_channel_messages), NotificationManager.IMPORTANCE_HIGH))
manager.createNotificationChannel(callNotificationChannel(CallChannel, generalGetString(R.string.ntf_channel_calls)))
// Remove old channels since they can't be edited
manager.deleteNotificationChannel("chat.simplex.app.CALL_NOTIFICATION")
manager.deleteNotificationChannel("chat.simplex.app.LOCK_SCREEN_CALL_NOTIFICATION")
}
/** /**
* Processes every action specified by [NotificationCompat.Builder.addAction] that comes with [NotificationAction] * Processes every action specified by [NotificationCompat.Builder.addAction] that comes with [NotificationAction]
* and [ChatInfo.id] as [ChatIdKey] in extra * and [ChatInfo.id] as [ChatIdKey] in extra
* */ * */
class NtfActionReceiver: BroadcastReceiver() { class NtfActionReceiver: BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) { override fun onReceive(context: Context?, intent: Intent?) {
val userId = getUserIdFromIntent(intent)
val chatId = intent?.getStringExtra(ChatIdKey) ?: return val chatId = intent?.getStringExtra(ChatIdKey) ?: return
val cInfo = SimplexApp.context.chatModel.getChat(chatId)?.chatInfo val m = SimplexApp.context.chatModel
when (intent.action) { when (intent.action) {
NotificationAction.ACCEPT_CONTACT_REQUEST.name -> { NotificationAction.ACCEPT_CONTACT_REQUEST.name -> {
if (cInfo !is ChatInfo.ContactRequest) return val isCurrentUser = m.currentUser.value?.userId == userId
acceptContactRequest(cInfo, SimplexApp.context.chatModel) val cInfo: ChatInfo.ContactRequest? = if (isCurrentUser) {
SimplexApp.context.chatModel.controller.ntfManager.cancelNotificationsForChat(chatId) (m.getChat(chatId)?.chatInfo as? ChatInfo.ContactRequest) ?: return
} else {
null
}
val apiId = chatId.replace("<@", "").toLongOrNull() ?: return
acceptContactRequest(apiId, cInfo, isCurrentUser, m)
m.controller.ntfManager.cancelNotificationsForChat(chatId)
} }
RejectCallAction -> { RejectCallAction -> {
val invitation = SimplexApp.context.chatModel.callInvitations[chatId] val invitation = m.callInvitations[chatId]
if (invitation != null) { if (invitation != null) {
SimplexApp.context.chatModel.callManager.endCall(invitation = invitation) m.callManager.endCall(invitation = invitation)
} }
} }
else -> { else -> {
File diff suppressed because it is too large Load Diff
@@ -18,7 +18,6 @@ val MessagePreviewDark = Color(179, 175, 174, 255)
val MessagePreviewLight = Color(49, 45, 44, 255) val MessagePreviewLight = Color(49, 45, 44, 255)
val ToolbarLight = Color(220, 220, 220, 12) val ToolbarLight = Color(220, 220, 220, 12)
val ToolbarDark = Color(80, 80, 80, 12) val ToolbarDark = Color(80, 80, 80, 12)
val SettingsBackgroundLight = Color(220, 216, 215, 90)
val SettingsSecondaryLight = Color(200, 196, 195, 90) val SettingsSecondaryLight = Color(200, 196, 195, 90)
val GroupDark = Color(80, 80, 80, 60) val GroupDark = Color(80, 80, 80, 60)
val IncomingCallLight = Color(239, 237, 236, 255) val IncomingCallLight = Color(239, 237, 236, 255)
@@ -27,3 +26,4 @@ val WarningOrange = Color(255, 127, 0, 255)
val WarningYellow = Color(255, 192, 0, 255) val WarningYellow = Color(255, 192, 0, 255)
val FileLight = Color(183, 190, 199, 255) val FileLight = Color(183, 190, 199, 255)
val FileDark = Color(101, 101, 106, 255) val FileDark = Color(101, 101, 106, 255)
val MenuTextColorDark = Color.White.copy(alpha = 0.8f)
@@ -8,4 +8,4 @@ val Shapes = Shapes(
small = RoundedCornerShape(4.dp), small = RoundedCornerShape(4.dp),
medium = RoundedCornerShape(4.dp), medium = RoundedCornerShape(4.dp),
large = RoundedCornerShape(0.dp) large = RoundedCornerShape(0.dp)
) )
@@ -2,55 +2,248 @@ package chat.simplex.app.ui.theme
import android.app.UiModeManager import android.app.UiModeManager
import android.content.Context import android.content.Context
import androidx.compose.foundation.background
import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material.* import androidx.compose.material.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.graphics.Color import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.*
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import chat.simplex.app.R
import chat.simplex.app.SimplexApp import chat.simplex.app.SimplexApp
import chat.simplex.app.views.helpers.*
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
enum class DefaultTheme { enum class DefaultTheme {
SYSTEM, DARK, LIGHT SYSTEM, LIGHT, DARK, SIMPLEX;
// Call it only with base theme, not SYSTEM
fun hasChangedAnyColor(colors: Colors, appColors: AppColors): Boolean {
val palette = when (this) {
SYSTEM -> return false
LIGHT -> LightColorPalette
DARK -> DarkColorPalette
SIMPLEX -> SimplexColorPalette
}
val appPalette = when (this) {
SYSTEM -> return false
LIGHT -> LightColorPaletteApp
DARK -> DarkColorPaletteApp
SIMPLEX -> SimplexColorPaletteApp
}
return colors.primary != palette.primary ||
colors.primaryVariant != palette.primaryVariant ||
colors.secondary != palette.secondary ||
colors.secondaryVariant != palette.secondaryVariant ||
colors.background != palette.background ||
colors.surface != palette.surface ||
appColors != appPalette
}
} }
val DEFAULT_PADDING = 16.dp data class AppColors(
val title: Color,
val sentMessage: Color,
val receivedMessage: Color
)
enum class ThemeColor {
PRIMARY, PRIMARY_VARIANT, SECONDARY, SECONDARY_VARIANT, BACKGROUND, SURFACE, TITLE, SENT_MESSAGE, RECEIVED_MESSAGE;
fun fromColors(colors: Colors, appColors: AppColors): Color {
return when (this) {
PRIMARY -> colors.primary
PRIMARY_VARIANT -> colors.primaryVariant
SECONDARY -> colors.secondary
SECONDARY_VARIANT -> colors.secondaryVariant
BACKGROUND -> colors.background
SURFACE -> colors.surface
TITLE -> appColors.title
SENT_MESSAGE -> appColors.sentMessage
RECEIVED_MESSAGE -> appColors.receivedMessage
}
}
val text: String
get() = when (this) {
PRIMARY -> generalGetString(R.string.color_primary)
PRIMARY_VARIANT -> generalGetString(R.string.color_primary_variant)
SECONDARY -> generalGetString(R.string.color_secondary)
SECONDARY_VARIANT -> generalGetString(R.string.color_secondary_variant)
BACKGROUND -> generalGetString(R.string.color_background)
SURFACE -> generalGetString(R.string.color_surface)
TITLE -> generalGetString(R.string.color_title)
SENT_MESSAGE -> generalGetString(R.string.color_sent_message)
RECEIVED_MESSAGE -> generalGetString(R.string.color_received_message)
}
}
@Serializable
data class ThemeColors(
@SerialName("accent")
val primary: String? = null,
@SerialName("accentVariant")
val primaryVariant: String? = null,
val secondary: String? = null,
val secondaryVariant: String? = null,
val background: String? = null,
@SerialName("menus")
val surface: String? = null,
val title: String? = null,
val sentMessage: String? = null,
val receivedMessage: String? = null,
) {
fun toColors(base: DefaultTheme): Colors {
val baseColors = when (base) {
DefaultTheme.LIGHT -> LightColorPalette
DefaultTheme.DARK -> DarkColorPalette
DefaultTheme.SIMPLEX -> SimplexColorPalette
// shouldn't be here
DefaultTheme.SYSTEM -> LightColorPalette
}
return baseColors.copy(
primary = primary?.colorFromReadableHex() ?: baseColors.primary,
primaryVariant = primaryVariant?.colorFromReadableHex() ?: baseColors.primaryVariant,
secondary = secondary?.colorFromReadableHex() ?: baseColors.secondary,
secondaryVariant = secondaryVariant?.colorFromReadableHex() ?: baseColors.secondaryVariant,
background = background?.colorFromReadableHex() ?: baseColors.background,
surface = surface?.colorFromReadableHex() ?: baseColors.surface,
)
}
fun toAppColors(base: DefaultTheme): AppColors {
val baseColors = when (base) {
DefaultTheme.LIGHT -> LightColorPaletteApp
DefaultTheme.DARK -> DarkColorPaletteApp
DefaultTheme.SIMPLEX -> SimplexColorPaletteApp
// shouldn't be here
DefaultTheme.SYSTEM -> LightColorPaletteApp
}
return baseColors.copy(
title = title?.colorFromReadableHex() ?: baseColors.title,
sentMessage = sentMessage?.colorFromReadableHex() ?: baseColors.sentMessage,
receivedMessage = receivedMessage?.colorFromReadableHex() ?: baseColors.receivedMessage,
)
}
}
private fun String.colorFromReadableHex(): Color =
Color(this.replace("#", "").toLongOrNull(16) ?: Color.White.toArgb().toLong())
@Serializable
data class ThemeOverrides (
val base: DefaultTheme,
val colors: ThemeColors
) {
fun withUpdatedColor(name: ThemeColor, color: String): ThemeOverrides {
return copy(colors = when (name) {
ThemeColor.PRIMARY -> colors.copy(primary = color)
ThemeColor.PRIMARY_VARIANT -> colors.copy(primaryVariant = color)
ThemeColor.SECONDARY -> colors.copy(secondary = color)
ThemeColor.SECONDARY_VARIANT -> colors.copy(secondaryVariant = color)
ThemeColor.BACKGROUND -> colors.copy(background = color)
ThemeColor.SURFACE -> colors.copy(surface = color)
ThemeColor.TITLE -> colors.copy(title = color)
ThemeColor.SENT_MESSAGE -> colors.copy(sentMessage = color)
ThemeColor.RECEIVED_MESSAGE -> colors.copy(receivedMessage = color)
})
}
}
@Serializable
data class ThemeData (val colors: ThemeColors)
fun Modifier.themedBackground(baseTheme: DefaultTheme = CurrentColors.value.base, shape: Shape = RectangleShape): Modifier {
return if (baseTheme == DefaultTheme.SIMPLEX) {
this.background(brush = Brush.linearGradient(
listOf(
CurrentColors.value.colors.background.darker(0.4f),
CurrentColors.value.colors.background.lighter(0.4f)
),
Offset(0f, Float.POSITIVE_INFINITY),
Offset(Float.POSITIVE_INFINITY, 0f)
), shape = shape)
} else {
this.background(color = CurrentColors.value.colors.background, shape = shape)
}
}
val DEFAULT_PADDING = 20.dp
val DEFAULT_SPACE_AFTER_ICON = 4.dp val DEFAULT_SPACE_AFTER_ICON = 4.dp
val DEFAULT_PADDING_HALF = DEFAULT_PADDING / 2 val DEFAULT_PADDING_HALF = DEFAULT_PADDING / 2
val DEFAULT_BOTTOM_PADDING = 48.dp
val DEFAULT_BOTTOM_BUTTON_PADDING = 20.dp
val DarkColorPalette = darkColors( val DarkColorPalette = darkColors(
primary = SimplexBlue, // If this value changes also need to update #0088ff in string resource files primary = SimplexBlue, // If this value changes also need to update #0088ff in string resource files
primaryVariant = SimplexGreen, primaryVariant = SimplexBlue,
secondary = DarkGray, secondary = HighOrLowlight,
secondaryVariant = DarkGray,
// background = Color.Black, // background = Color.Black,
// surface = Color.Black, surface = Color(0xFF222222),
// background = Color(0xFF121212), // background = Color(0xFF121212),
// surface = Color(0xFF121212), // surface = Color(0xFF121212),
// error = Color(0xFFCF6679), error = Color.Red,
onBackground = Color(0xFFFFFBFA), onBackground = Color(0xFFFFFBFA),
onSurface = Color(0xFFFFFBFA), onSurface = Color(0xFFFFFBFA),
// onError: Color = Color.Black, // onError: Color = Color.Black,
) )
val DarkColorPaletteApp = AppColors(
title = SimplexBlue,
sentMessage = Color(0x1E45B8FF),
receivedMessage = Color(0x20B1B0B5)
)
val LightColorPalette = lightColors( val LightColorPalette = lightColors(
primary = SimplexBlue, // If this value changes also need to update #0088ff in string resource files primary = SimplexBlue, // If this value changes also need to update #0088ff in string resource files
primaryVariant = SimplexGreen, primaryVariant = SimplexBlue,
secondary = LightGray, secondary = HighOrLowlight,
secondaryVariant = LightGray,
error = Color.Red,
// background = Color.White, // background = Color.White,
// surface = Color.White surface = Color.White,
// onPrimary = Color.White, // onPrimary = Color.White,
// onSecondary = Color.Black, // onSecondary = Color.Black,
// onBackground = Color.Black, // onBackground = Color.Black,
// onSurface = Color.Black, // onSurface = Color.Black,
) )
val LightColorPaletteApp = AppColors(
title = SimplexBlue,
sentMessage = Color(0x1E45B8FF),
receivedMessage = Color(0x20B1B0B5)
)
val CurrentColors: MutableStateFlow<Pair<Colors, DefaultTheme>> = MutableStateFlow(ThemeManager.currentColors(isInNightMode())) val SimplexColorPalette = darkColors(
primary = Color(0xFF70F0F9), // If this value changes also need to update #0088ff in string resource files
primaryVariant = Color(0xFF1298A5),
secondary = HighOrLowlight,
secondaryVariant = Color(0xFF2C464D),
background = Color(0xFF111528),
// surface = Color.Black,
// background = Color(0xFF121212),
surface = Color(0xFF121C37),
error = Color.Red,
// onBackground = Color(0xFFFFFBFA),
// onSurface = Color(0xFFFFFBFA),
// onError: Color = Color.Black,
)
val SimplexColorPaletteApp = AppColors(
title = Color(0xFF267BE5),
sentMessage = Color(0x1E45B8FF),
receivedMessage = Color(0x20B1B0B5)
)
val CurrentColors: MutableStateFlow<ThemeManager.ActiveTheme> = MutableStateFlow(ThemeManager.currentColors(isInNightMode()))
// Non-@Composable implementation // Non-@Composable implementation
private fun isInNightMode() = private fun isInNightMode() =
(SimplexApp.context.getSystemService(Context.UI_MODE_SERVICE) as UiModeManager).nightMode == UiModeManager.MODE_NIGHT_YES (SimplexApp.context.getSystemService(Context.UI_MODE_SERVICE) as UiModeManager).nightMode == UiModeManager.MODE_NIGHT_YES
@Composable @Composable
fun isInDarkTheme(): Boolean = !CurrentColors.collectAsState().value.first.isLight fun isInDarkTheme(): Boolean = !CurrentColors.collectAsState().value.colors.isLight
@Composable @Composable
fun SimpleXTheme(darkTheme: Boolean? = null, content: @Composable () -> Unit) { fun SimpleXTheme(darkTheme: Boolean? = null, content: @Composable () -> Unit) {
@@ -61,16 +254,16 @@ fun SimpleXTheme(darkTheme: Boolean? = null, content: @Composable () -> Unit) {
} }
val systemDark = isSystemInDarkTheme() val systemDark = isSystemInDarkTheme()
LaunchedEffect(systemDark) { LaunchedEffect(systemDark) {
if (CurrentColors.value.second == DefaultTheme.SYSTEM && CurrentColors.value.first.isLight == systemDark) { if (SimplexApp.context.chatModel.controller.appPrefs.currentTheme.get() == DefaultTheme.SYSTEM.name && CurrentColors.value.colors.isLight == systemDark) {
// Change active colors from light to dark and back based on system theme // Change active colors from light to dark and back based on system theme
ThemeManager.applyTheme(DefaultTheme.SYSTEM.name, systemDark) ThemeManager.applyTheme(DefaultTheme.SYSTEM.name, systemDark)
} }
} }
val theme by CurrentColors.collectAsState() val theme by CurrentColors.collectAsState()
MaterialTheme( MaterialTheme(
colors = theme.first, colors = theme.colors,
typography = Typography, typography = Typography,
shapes = Shapes, shapes = Shapes,
content = content content = content
) )
} }
@@ -7,22 +7,56 @@ import chat.simplex.app.R
import chat.simplex.app.SimplexApp import chat.simplex.app.SimplexApp
import chat.simplex.app.model.AppPreferences import chat.simplex.app.model.AppPreferences
import chat.simplex.app.views.helpers.generalGetString import chat.simplex.app.views.helpers.generalGetString
import okhttp3.internal.toHexString
object ThemeManager { object ThemeManager {
private val appPrefs: AppPreferences by lazy { private val appPrefs: AppPreferences by lazy {
AppPreferences(SimplexApp.context) SimplexApp.context.chatModel.controller.appPrefs
} }
fun currentColors(darkForSystemTheme: Boolean): Pair<Colors, DefaultTheme> { data class ActiveTheme(val name: String, val base: DefaultTheme, val colors: Colors, val appColors: AppColors)
val theme = appPrefs.currentTheme.get()!!
val systemThemeColors = if (darkForSystemTheme) DarkColorPalette else LightColorPalette private fun systemDarkThemeColors(): Pair<Colors, DefaultTheme> = when (appPrefs.systemDarkTheme.get()) {
val res = when (theme) { DefaultTheme.DARK.name -> DarkColorPalette to DefaultTheme.DARK
DefaultTheme.SYSTEM.name -> Pair(systemThemeColors, DefaultTheme.SYSTEM) DefaultTheme.SIMPLEX.name -> SimplexColorPalette to DefaultTheme.SIMPLEX
DefaultTheme.DARK.name -> Pair(DarkColorPalette, DefaultTheme.DARK) else -> SimplexColorPalette to DefaultTheme.SIMPLEX
DefaultTheme.LIGHT.name -> Pair(LightColorPalette, DefaultTheme.LIGHT) }
else -> Pair(systemThemeColors, DefaultTheme.SYSTEM)
fun currentColors(darkForSystemTheme: Boolean): ActiveTheme {
val themeName = appPrefs.currentTheme.get()!!
val themeOverrides = appPrefs.themeOverrides.get()
val nonSystemThemeName = if (themeName != DefaultTheme.SYSTEM.name) {
themeName
} else {
if (darkForSystemTheme) appPrefs.systemDarkTheme.get()!! else DefaultTheme.LIGHT.name
} }
return res.copy(first = res.first.copy(primary = Color(appPrefs.primaryColor.get()))) val theme = themeOverrides[nonSystemThemeName]
val baseTheme = when (nonSystemThemeName) {
DefaultTheme.LIGHT.name -> Triple(DefaultTheme.LIGHT, LightColorPalette, LightColorPaletteApp)
DefaultTheme.DARK.name -> Triple(DefaultTheme.DARK, DarkColorPalette, DarkColorPaletteApp)
DefaultTheme.SIMPLEX.name -> Triple(DefaultTheme.SIMPLEX, SimplexColorPalette, SimplexColorPaletteApp)
else -> Triple(DefaultTheme.LIGHT, LightColorPalette, LightColorPaletteApp)
}
if (theme == null) {
return ActiveTheme(themeName, baseTheme.first, baseTheme.second, baseTheme.third)
}
return ActiveTheme(themeName, baseTheme.first, theme.colors.toColors(theme.base), theme.colors.toAppColors(theme.base))
}
fun currentThemeData(darkForSystemTheme: Boolean): ThemeData {
val t = currentColors(darkForSystemTheme)
return ThemeData(colors = ThemeColors(
primary = t.colors.primary.toReadableHex(),
primaryVariant = t.colors.primaryVariant.toReadableHex(),
secondary = t.colors.secondary.toReadableHex(),
secondaryVariant = t.colors.secondaryVariant.toReadableHex(),
background = t.colors.background.toReadableHex(),
surface = t.colors.surface.toReadableHex(),
title = t.appColors.title.toReadableHex(),
sentMessage = t.appColors.sentMessage.toReadableHex(),
receivedMessage = t.appColors.receivedMessage.toReadableHex()
))
} }
// colors, default theme enum, localized name of theme // colors, default theme enum, localized name of theme
@@ -30,7 +64,7 @@ object ThemeManager {
val allThemes = ArrayList<Triple<Colors, DefaultTheme, String>>() val allThemes = ArrayList<Triple<Colors, DefaultTheme, String>>()
allThemes.add( allThemes.add(
Triple( Triple(
if (darkForSystemTheme) DarkColorPalette else LightColorPalette, if (darkForSystemTheme) systemDarkThemeColors().first else LightColorPalette,
DefaultTheme.SYSTEM, DefaultTheme.SYSTEM,
generalGetString(R.string.theme_system) generalGetString(R.string.theme_system)
) )
@@ -49,16 +83,72 @@ object ThemeManager {
generalGetString(R.string.theme_dark) generalGetString(R.string.theme_dark)
) )
) )
allThemes.add(
Triple(
SimplexColorPalette,
DefaultTheme.SIMPLEX,
generalGetString(R.string.theme_simplex)
)
)
return allThemes return allThemes
} }
fun applyTheme(name: String, darkForSystemTheme: Boolean) { fun applyTheme(theme: String, darkForSystemTheme: Boolean) {
appPrefs.currentTheme.set(name) appPrefs.currentTheme.set(theme)
CurrentColors.value = currentColors(darkForSystemTheme) CurrentColors.value = currentColors(darkForSystemTheme)
} }
fun saveAndApplyPrimaryColor(color: Color) { fun changeDarkTheme(theme: String, darkForSystemTheme: Boolean) {
appPrefs.primaryColor.set(color.toArgb()) appPrefs.systemDarkTheme.set(theme)
CurrentColors.value = currentColors(!CurrentColors.value.first.isLight) CurrentColors.value = currentColors(darkForSystemTheme)
}
fun saveAndApplyThemeColor(name: ThemeColor, color: Color? = null, darkForSystemTheme: Boolean) {
val themeName = appPrefs.currentTheme.get()!!
val nonSystemThemeName = if (themeName != DefaultTheme.SYSTEM.name) {
themeName
} else {
if (darkForSystemTheme) appPrefs.systemDarkTheme.get()!! else DefaultTheme.LIGHT.name
}
var colorToSet = color
if (colorToSet == null) {
// Setting default color from a base theme
colorToSet = when(nonSystemThemeName) {
DefaultTheme.LIGHT.name -> name.fromColors(LightColorPalette, LightColorPaletteApp)
DefaultTheme.DARK.name -> name.fromColors(DarkColorPalette, DarkColorPaletteApp)
DefaultTheme.SIMPLEX.name -> name.fromColors(SimplexColorPalette, SimplexColorPaletteApp)
// Will not be here
else -> return
}
}
val overrides = appPrefs.themeOverrides.get().toMutableMap()
val prevValue = overrides[nonSystemThemeName] ?: ThemeOverrides(base = CurrentColors.value.base, colors = ThemeColors())
overrides[nonSystemThemeName] = prevValue.withUpdatedColor(name, colorToSet.toReadableHex())
appPrefs.themeOverrides.set(overrides)
CurrentColors.value = currentColors(!CurrentColors.value.colors.isLight)
}
fun saveAndApplyThemeData(name: String, theme: ThemeData, darkForSystemTheme: Boolean) {
val overrides = appPrefs.themeOverrides.get().toMutableMap()
val prevValue = overrides[name] ?: ThemeOverrides(base = CurrentColors.value.base, colors = ThemeColors())
overrides[name] = prevValue.copy(colors = theme.colors)
appPrefs.themeOverrides.set(overrides)
CurrentColors.value = currentColors(!CurrentColors.value.colors.isLight)
}
fun resetAllThemeColors(darkForSystemTheme: Boolean) {
val themeName = appPrefs.currentTheme.get()!!
val nonSystemThemeName = if (themeName != DefaultTheme.SYSTEM.name) {
themeName
} else {
if (darkForSystemTheme) appPrefs.systemDarkTheme.get()!! else DefaultTheme.LIGHT.name
}
val overrides = appPrefs.themeOverrides.get().toMutableMap()
val prevValue = overrides[nonSystemThemeName] ?: return
overrides[nonSystemThemeName] = prevValue.copy(colors = ThemeColors())
appPrefs.themeOverrides.set(overrides)
CurrentColors.value = currentColors(!CurrentColors.value.colors.isLight)
} }
} }
private fun Color.toReadableHex(): String = "#" + toArgb().toHexString()
@@ -1,6 +1,5 @@
package chat.simplex.app.views package chat.simplex.app.views
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface import androidx.compose.material.Surface
@@ -11,8 +10,8 @@ import androidx.compose.ui.Modifier
fun SplashView() { fun SplashView() {
Surface( Surface(
Modifier Modifier
.background(MaterialTheme.colors.background) .fillMaxSize(),
.fillMaxSize() color = MaterialTheme.colors.background
) { ) {
// Image( // Image(
// painter = painterResource(R.drawable.logo), // painter = painterResource(R.drawable.logo),
@@ -1,29 +1,21 @@
package chat.simplex.app.views package chat.simplex.app.views
import android.content.Context
import android.content.res.Configuration import android.content.res.Configuration
import android.os.SystemClock
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.compose.foundation.* import androidx.compose.foundation.*
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.* import androidx.compose.foundation.lazy.*
import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material.* import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Lock
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.fragment.app.FragmentActivity
import chat.simplex.app.R
import chat.simplex.app.model.* import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.* import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.chat.* import chat.simplex.app.views.chat.*
@@ -31,70 +23,18 @@ import chat.simplex.app.views.helpers.*
import com.google.accompanist.insets.ProvideWindowInsets import com.google.accompanist.insets.ProvideWindowInsets
import com.google.accompanist.insets.navigationBarsWithImePadding import com.google.accompanist.insets.navigationBarsWithImePadding
private val lastSuccessfulAuth: MutableState<Long?> = mutableStateOf(null)
@Composable @Composable
fun TerminalView(chatModel: ChatModel, close: () -> Unit) { fun TerminalView(chatModel: ChatModel, close: () -> Unit) {
val composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = false)) } val composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = false)) }
val lastSuccessfulAuth = remember { lastSuccessfulAuth }
BackHandler(onBack = { BackHandler(onBack = {
lastSuccessfulAuth.value = null
close() close()
}) })
val authorized = remember { !chatModel.controller.appPrefs.performLA.get() }
val context = LocalContext.current
LaunchedEffect(lastSuccessfulAuth.value) {
if (!authorized && !authorizedPreviously(lastSuccessfulAuth)) {
runAuth(lastSuccessfulAuth, context)
}
}
if (authorized || authorizedPreviously(lastSuccessfulAuth)) {
LaunchedEffect(Unit) {
// Update auth each time user visits this screen in authenticated state just to prolong authorized time
lastSuccessfulAuth.value = SystemClock.elapsedRealtime()
}
TerminalLayout( TerminalLayout(
chatModel.terminalItems, remember { chatModel.terminalItems },
composeState, composeState,
sendCommand = { sendCommand(chatModel, composeState) }, sendCommand = { sendCommand(chatModel, composeState) },
close close
) )
} else {
Surface(Modifier.fillMaxSize()) {
Column(Modifier.background(MaterialTheme.colors.background)) {
CloseSheetBar(close)
Box(
Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
SimpleButton(
stringResource(R.string.auth_unlock),
icon = Icons.Outlined.Lock,
click = {
runAuth(lastSuccessfulAuth, context)
}
)
}
}
}
}
}
private fun authorizedPreviously(lastSuccessfulAuth: State<Long?>): Boolean =
lastSuccessfulAuth.value?.let { SystemClock.elapsedRealtime() - it < 30_000 } ?: false
private fun runAuth(lastSuccessfulAuth: MutableState<Long?>, context: Context) {
authenticate(
generalGetString(R.string.auth_open_chat_console),
generalGetString(R.string.auth_log_in_using_credential),
context as FragmentActivity,
completed = { laResult ->
lastSuccessfulAuth.value = when (laResult) {
LAResult.Success, LAResult.Unavailable -> SystemClock.elapsedRealtime()
is LAResult.Error, LAResult.Failed -> null
}
}
)
} }
private fun sendCommand(chatModel: ChatModel, composeState: MutableState<ComposeState>) { private fun sendCommand(chatModel: ChatModel, composeState: MutableState<ComposeState>) {
@@ -102,9 +42,9 @@ private fun sendCommand(chatModel: ChatModel, composeState: MutableState<Compose
val prefPerformLA = chatModel.controller.appPrefs.performLA.get() val prefPerformLA = chatModel.controller.appPrefs.performLA.get()
val s = composeState.value.message val s = composeState.value.message
if (s.startsWith("/sql") && (!prefPerformLA || !developerTools)) { if (s.startsWith("/sql") && (!prefPerformLA || !developerTools)) {
val resp = CR.ChatCmdError(ChatError.ChatErrorChat(ChatErrorType.СommandError("Failed reading: empty"))) val resp = CR.ChatCmdError(null, ChatError.ChatErrorChat(ChatErrorType.СommandError("Failed reading: empty")))
chatModel.terminalItems.add(TerminalItem.cmd(CC.Console(s))) chatModel.addTerminalItem(TerminalItem.cmd(CC.Console(s)))
chatModel.terminalItems.add(TerminalItem.resp(resp)) chatModel.addTerminalItem(TerminalItem.resp(resp))
composeState.value = ComposeState(useLinkPreviews = false) composeState.value = ComposeState(useLinkPreviews = false)
} else { } else {
withApi { withApi {
@@ -143,6 +83,8 @@ fun TerminalLayout(
liveMessageAlertShown = SharedPreference(get = { false }, set = {}), liveMessageAlertShown = SharedPreference(get = { false }, set = {}),
needToAllowVoiceToContact = false, needToAllowVoiceToContact = false,
allowedVoiceByPrefs = false, allowedVoiceByPrefs = false,
userIsObserver = false,
userCanSend = true,
allowVoiceToContact = {}, allowVoiceToContact = {},
sendMessage = sendCommand, sendMessage = sendCommand,
sendLiveMessage = null, sendLiveMessage = null,
@@ -158,7 +100,7 @@ fun TerminalLayout(
modifier = Modifier modifier = Modifier
.padding(contentPadding) .padding(contentPadding)
.fillMaxWidth() .fillMaxWidth()
.background(MaterialTheme.colors.background) .themedBackground()
) { ) {
TerminalLog(terminalItems) TerminalLog(terminalItems)
} }
@@ -175,6 +117,7 @@ fun TerminalLog(terminalItems: List<TerminalItem>) {
onDispose { lazyListState = listState.firstVisibleItemIndex to listState.firstVisibleItemScrollOffset } onDispose { lazyListState = listState.firstVisibleItemIndex to listState.firstVisibleItemScrollOffset }
} }
val reversedTerminalItems by remember { derivedStateOf { terminalItems.reversed().toList() } } val reversedTerminalItems by remember { derivedStateOf { terminalItems.reversed().toList() } }
val context = LocalContext.current
LazyColumn(state = listState, reverseLayout = true) { LazyColumn(state = listState, reverseLayout = true) {
items(reversedTerminalItems) { item -> items(reversedTerminalItems) { item ->
Text( Text(
@@ -185,7 +128,7 @@ fun TerminalLog(terminalItems: List<TerminalItem>) {
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.clickable { .clickable {
ModalManager.shared.showModal { ModalManager.shared.showModal(endButtons = { ShareButton { shareText(context, item.details) } }) {
SelectionContainer(modifier = Modifier.verticalScroll(rememberScrollState())) { SelectionContainer(modifier = Modifier.verticalScroll(rememberScrollState())) {
Text(item.details, modifier = Modifier.padding(horizontal = DEFAULT_PADDING).padding(bottom = DEFAULT_PADDING)) Text(item.details, modifier = Modifier.padding(horizontal = DEFAULT_PADDING).padding(bottom = DEFAULT_PADDING))
} }
@@ -6,93 +6,107 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.* import androidx.compose.material.*
import androidx.compose.material.icons.Icons import androidx.compose.material.MaterialTheme.colors
import androidx.compose.material.icons.outlined.ArrowBackIosNew
import androidx.compose.material.icons.outlined.ArrowForwardIos
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.focus.*
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.style.*
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import chat.simplex.app.*
import chat.simplex.app.R import chat.simplex.app.R
import chat.simplex.app.SimplexService
import chat.simplex.app.model.ChatModel import chat.simplex.app.model.ChatModel
import chat.simplex.app.model.Profile import chat.simplex.app.model.Profile
import chat.simplex.app.ui.theme.* import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.helpers.AppBarTitle import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.helpers.withApi
import chat.simplex.app.views.onboarding.OnboardingStage import chat.simplex.app.views.onboarding.OnboardingStage
import chat.simplex.app.views.onboarding.ReadableText import chat.simplex.app.views.onboarding.ReadableText
import com.google.accompanist.insets.navigationBarsWithImePadding import com.google.accompanist.insets.navigationBarsWithImePadding
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.distinctUntilChanged
fun isValidDisplayName(name: String) : Boolean { fun isValidDisplayName(name: String) : Boolean {
return (name.firstOrNull { it.isWhitespace() }) == null return (name.firstOrNull { it.isWhitespace() }) == null && !name.startsWith("@") && !name.startsWith("#")
} }
@Composable @Composable
fun CreateProfilePanel(chatModel: ChatModel) { fun CreateProfilePanel(chatModel: ChatModel, close: () -> Unit) {
val displayName = remember { mutableStateOf("") } val displayName = rememberSaveable { mutableStateOf("") }
val fullName = remember { mutableStateOf("") } val fullName = rememberSaveable { mutableStateOf("") }
val focusRequester = remember { FocusRequester() } val focusRequester = remember { FocusRequester() }
Surface(Modifier.background(MaterialTheme.colors.onBackground)) { Surface(Modifier.background(MaterialTheme.colors.onBackground)) {
Column( Column(
modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()) modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState())
) { ) {
AppBarTitle(stringResource(R.string.create_profile), false) /*CloseSheetBar(close = {
ReadableText(R.string.your_profile_is_stored_on_your_device) if (chatModel.users.isEmpty()) {
ReadableText(R.string.profile_is_only_shared_with_your_contacts) chatModel.onboardingStage.value = OnboardingStage.Step1_SimpleXInfo
Spacer(Modifier.height(10.dp)) } else {
Text( close()
stringResource(R.string.display_name), }
style = MaterialTheme.typography.h6, })*/
modifier = Modifier.padding(bottom = 3.dp) Column(Modifier.padding(horizontal = DEFAULT_PADDING * 1f)) {
) AppBarTitle(stringResource(R.string.create_profile))
ProfileNameField(displayName, focusRequester) ReadableText(R.string.your_profile_is_stored_on_your_device, TextAlign.Center, padding = PaddingValues())
val errorText = if (!isValidDisplayName(displayName.value)) stringResource(R.string.display_name_cannot_contain_whitespace) else "" ReadableText(R.string.profile_is_only_shared_with_your_contacts, TextAlign.Center)
Text( Spacer(Modifier.height(DEFAULT_PADDING * 1.5f))
errorText, Row(Modifier.padding(bottom = DEFAULT_PADDING_HALF).fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
fontSize = 15.sp, Text(
color = MaterialTheme.colors.error stringResource(R.string.display_name),
) fontSize = 16.sp
Spacer(Modifier.height(3.dp)) )
Text( if (!isValidDisplayName(displayName.value)) {
stringResource(R.string.full_name_optional__prompt), Text(
style = MaterialTheme.typography.h6, stringResource(R.string.no_spaces),
modifier = Modifier.padding(bottom = 5.dp) fontSize = 16.sp,
) color = Color.Red
ProfileNameField(fullName) )
}
}
ProfileNameField(displayName, "", ::isValidDisplayName, focusRequester)
Spacer(Modifier.height(DEFAULT_PADDING))
Text(
stringResource(R.string.full_name_optional__prompt),
fontSize = 16.sp,
modifier = Modifier.padding(bottom = DEFAULT_PADDING_HALF)
)
ProfileNameField(fullName, "", ::isValidDisplayName)
}
Spacer(Modifier.fillMaxHeight().weight(1f)) Spacer(Modifier.fillMaxHeight().weight(1f))
Row { Row {
SimpleButton( if (chatModel.users.isEmpty()) {
text = stringResource(R.string.about_simplex), SimpleButtonDecorated(
icon = Icons.Outlined.ArrowBackIosNew text = stringResource(R.string.about_simplex),
) { chatModel.onboardingStage.value = OnboardingStage.Step1_SimpleXInfo } icon = painterResource(R.drawable.ic_arrow_back_ios_new),
textDecoration = TextDecoration.None,
fontWeight = FontWeight.Medium
) { chatModel.onboardingStage.value = OnboardingStage.Step1_SimpleXInfo }
}
Spacer(Modifier.fillMaxWidth().weight(1f)) Spacer(Modifier.fillMaxWidth().weight(1f))
val enabled = displayName.value.isNotEmpty() && isValidDisplayName(displayName.value) val enabled = displayName.value.isNotEmpty() && isValidDisplayName(displayName.value)
val createModifier: Modifier val createModifier: Modifier
val createColor: Color val createColor: Color
if (enabled) { if (enabled) {
createModifier = Modifier.clickable { createProfile(chatModel, displayName.value, fullName.value) }.padding(8.dp) createModifier = Modifier.clickable { createProfile(chatModel, displayName.value, fullName.value, close) }.padding(8.dp)
createColor = MaterialTheme.colors.primary createColor = MaterialTheme.colors.primary
} else { } else {
createModifier = Modifier.padding(8.dp) createModifier = Modifier.padding(8.dp)
createColor = HighOrLowlight createColor = MaterialTheme.colors.secondary
} }
Surface(shape = RoundedCornerShape(20.dp)) { Surface(shape = RoundedCornerShape(20.dp)) {
Row(verticalAlignment = Alignment.CenterVertically, modifier = createModifier) { Row(verticalAlignment = Alignment.CenterVertically, modifier = createModifier) {
Text(stringResource(R.string.create_profile_button), style = MaterialTheme.typography.caption, color = createColor) Text(stringResource(R.string.create_profile_button), style = MaterialTheme.typography.caption, color = createColor, fontWeight = FontWeight.Medium)
Icon(Icons.Outlined.ArrowForwardIos, stringResource(R.string.create_profile_button), tint = createColor) Icon(painterResource(R.drawable.ic_arrow_forward_ios), stringResource(R.string.create_profile_button), tint = createColor)
} }
} }
} }
@@ -105,35 +119,71 @@ fun CreateProfilePanel(chatModel: ChatModel) {
} }
} }
fun createProfile(chatModel: ChatModel, displayName: String, fullName: String) { fun createProfile(chatModel: ChatModel, displayName: String, fullName: String, close: () -> Unit) {
withApi { withApi {
val user = chatModel.controller.apiCreateActiveUser( val user = chatModel.controller.apiCreateActiveUser(
Profile(displayName, fullName, null) Profile(displayName, fullName, null)
) ) ?: return@withApi
chatModel.controller.startChat(user) chatModel.currentUser.value = user
chatModel.onboardingStage.value = OnboardingStage.Step3_SetNotificationsMode if (chatModel.users.isEmpty()) {
chatModel.controller.startChat(user)
chatModel.onboardingStage.value = OnboardingStage.Step3_SetNotificationsMode
SimplexApp.context.chatModel.controller.ntfManager.createNtfChannelsMaybeShowAlert()
} else {
val users = chatModel.controller.listUsers()
chatModel.users.clear()
chatModel.users.addAll(users)
chatModel.controller.getUserChatData()
close()
}
} }
} }
@Composable @Composable
fun ProfileNameField(name: MutableState<String>, focusRequester: FocusRequester? = null) { fun ProfileNameField(name: MutableState<String>, placeholder: String = "", isValid: (String) -> Boolean = { true }, focusRequester: FocusRequester? = null) {
var valid by rememberSaveable { mutableStateOf(true) }
var focused by rememberSaveable { mutableStateOf(false) }
val strokeColor by remember {
derivedStateOf {
if (valid) {
if (focused) {
CurrentColors.value.colors.secondary.copy(alpha = 0.6f)
} else {
CurrentColors.value.colors.secondary.copy(alpha = 0.3f)
}
} else Color.Red
}
}
val modifier = Modifier val modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.background(MaterialTheme.colors.secondary) .padding(horizontal = DEFAULT_PADDING)
.height(40.dp)
.clip(RoundedCornerShape(5.dp))
.padding(8.dp)
.navigationBarsWithImePadding() .navigationBarsWithImePadding()
BasicTextField( .onFocusChanged { focused = it.isFocused }
value = name.value, Box(
onValueChange = { name.value = it }, Modifier
modifier = if (focusRequester == null) modifier else modifier.focusRequester(focusRequester), .fillMaxWidth()
textStyle = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.onBackground), .height(52.dp)
keyboardOptions = KeyboardOptions( .border(border = BorderStroke(1.dp, strokeColor), shape = RoundedCornerShape(50)),
capitalization = KeyboardCapitalization.None, contentAlignment = Alignment.Center
autoCorrect = false ) {
), BasicTextField(
singleLine = true, value = name.value,
cursorBrush = SolidColor(HighOrLowlight) onValueChange = { name.value = it },
) modifier = if (focusRequester == null) modifier else modifier.focusRequester(focusRequester),
} textStyle = TextStyle(fontSize = 18.sp, color = colors.onBackground),
keyboardOptions = KeyboardOptions(
capitalization = KeyboardCapitalization.None,
autoCorrect = false
),
singleLine = true,
cursorBrush = SolidColor(MaterialTheme.colors.secondary)
)
}
LaunchedEffect(Unit) {
snapshotFlow { name.value }
.distinctUntilChanged()
.collect {
valid = isValid(it)
}
}
}
@@ -13,12 +13,14 @@ class CallManager(val chatModel: ChatModel) {
Log.d(TAG, "CallManager.reportNewIncomingCall") Log.d(TAG, "CallManager.reportNewIncomingCall")
with (chatModel) { with (chatModel) {
callInvitations[invitation.contact.id] = invitation callInvitations[invitation.contact.id] = invitation
if (Clock.System.now() - invitation.callTs <= 3.minutes) { if (invitation.user.showNotifications) {
activeCallInvitation.value = invitation if (Clock.System.now() - invitation.callTs <= 3.minutes) {
controller.ntfManager.notifyCallInvitation(invitation) activeCallInvitation.value = invitation
} else { controller.ntfManager.notifyCallInvitation(invitation)
val contact = invitation.contact } else {
controller.ntfManager.notifyMessageReceived(chatId = contact.id, displayName = contact.displayName, msgText = invitation.callTypeText) val contact = invitation.contact
controller.ntfManager.displayNotification(user = invitation.user, chatId = contact.id, displayName = contact.displayName, msgText = invitation.callTypeText)
}
} }
} }
} }
@@ -104,4 +106,4 @@ class CallManager(val chatModel: ChatModel) {
chatModel.controller.ntfManager.cancelCallNotification() chatModel.controller.ntfManager.cancelCallNotification()
} }
} }
} }
@@ -3,11 +3,12 @@ package chat.simplex.app.views.call
import android.Manifest import android.Manifest
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
import android.content.Context import android.content.*
import android.content.pm.ActivityInfo import android.content.pm.ActivityInfo
import android.media.AudioDeviceInfo import android.media.*
import android.media.AudioManager
import android.os.Build import android.os.Build
import android.os.PowerManager
import android.os.PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK
import android.util.Log import android.util.Log
import android.view.ViewGroup import android.view.ViewGroup
import android.webkit.* import android.webkit.*
@@ -15,16 +16,15 @@ import androidx.activity.compose.BackHandler
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.material.* import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material.icons.outlined.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
@@ -37,7 +37,7 @@ import androidx.webkit.WebViewClientCompat
import chat.simplex.app.* import chat.simplex.app.*
import chat.simplex.app.R import chat.simplex.app.R
import chat.simplex.app.model.* import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.SimpleXTheme import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.helpers.ProfileImage import chat.simplex.app.views.helpers.ProfileImage
import chat.simplex.app.views.helpers.withApi import chat.simplex.app.views.helpers.withApi
import chat.simplex.app.views.usersettings.NotificationsMode import chat.simplex.app.views.usersettings.NotificationsMode
@@ -53,6 +53,7 @@ fun ActiveCallView(chatModel: ChatModel) {
val call = chatModel.activeCall.value val call = chatModel.activeCall.value
if (call != null) withApi { chatModel.callManager.endCall(call) } if (call != null) withApi { chatModel.callManager.endCall(call) }
}) })
val audioViaBluetooth = rememberSaveable { mutableStateOf(false) }
val ntfModeService = remember { chatModel.controller.appPrefs.notificationsMode.get() == NotificationsMode.SERVICE.name } val ntfModeService = remember { chatModel.controller.appPrefs.notificationsMode.get() == NotificationsMode.SERVICE.name }
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
// Start service when call happening since it's not already started. // Start service when call happening since it's not already started.
@@ -60,17 +61,48 @@ fun ActiveCallView(chatModel: ChatModel) {
if (!ntfModeService) SimplexService.start(SimplexApp.context) if (!ntfModeService) SimplexService.start(SimplexApp.context)
} }
DisposableEffect(Unit) { DisposableEffect(Unit) {
val am = SimplexApp.context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
var btDeviceCount = 0
val audioCallback = object: AudioDeviceCallback() {
override fun onAudioDevicesAdded(addedDevices: Array<out AudioDeviceInfo>) {
Log.d(TAG, "Added audio devices: ${addedDevices.map { it.type }}")
super.onAudioDevicesAdded(addedDevices)
val addedCount = addedDevices.count { it.type == AudioDeviceInfo.TYPE_BLUETOOTH_SCO }
btDeviceCount += addedCount
audioViaBluetooth.value = btDeviceCount > 0
if (addedCount > 0 && chatModel.activeCall.value?.callState == CallState.Connected) {
// Setting params in Connected state makes sure that Bluetooth will NOT be broken on Android < 12
setCallSound(chatModel.activeCall.value?.soundSpeaker ?: return, audioViaBluetooth)
}
}
override fun onAudioDevicesRemoved(removedDevices: Array<out AudioDeviceInfo>) {
Log.d(TAG, "Removed audio devices: ${removedDevices.map { it.type }}")
super.onAudioDevicesRemoved(removedDevices)
val removedCount = removedDevices.count { it.type == AudioDeviceInfo.TYPE_BLUETOOTH_SCO }
btDeviceCount -= removedCount
audioViaBluetooth.value = btDeviceCount > 0
if (btDeviceCount == 0 && chatModel.activeCall.value?.callState == CallState.Connected) {
// Setting params in Connected state makes sure that Bluetooth will NOT be broken on Android < 12
setCallSound(chatModel.activeCall.value?.soundSpeaker ?: return, audioViaBluetooth)
}
}
}
am.registerAudioDeviceCallback(audioCallback, null)
val pm = (SimplexApp.context.getSystemService(Context.POWER_SERVICE) as PowerManager)
val proximityLock = if (pm.isWakeLockLevelSupported(PROXIMITY_SCREEN_OFF_WAKE_LOCK)) {
pm.newWakeLock(PROXIMITY_SCREEN_OFF_WAKE_LOCK, SimplexApp.context.packageName + ":proximityLock")
} else {
null
}
proximityLock?.acquire()
onDispose { onDispose {
// Stop it when call ended // Stop it when call ended
if (!ntfModeService) SimplexService.safeStopService(SimplexApp.context) if (!ntfModeService) SimplexService.safeStopService(SimplexApp.context)
// Clear selected communication device to default value after we changed it in call dropAudioManagerOverrides()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { am.unregisterAudioDeviceCallback(audioCallback)
val am = SimplexApp.context.getSystemService(Context.AUDIO_SERVICE) as AudioManager proximityLock?.release()
am.clearCommunicationDevice()
}
} }
} }
val cxt = LocalContext.current
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
Box(Modifier.fillMaxSize()) { Box(Modifier.fillMaxSize()) {
WebRTCView(chatModel.callCommand) { apiMsg -> WebRTCView(chatModel.callCommand) { apiMsg ->
@@ -100,6 +132,7 @@ fun ActiveCallView(chatModel: ChatModel) {
val callStatus = json.decodeFromString<WebRTCCallStatus>("\"${r.state.connectionState}\"") val callStatus = json.decodeFromString<WebRTCCallStatus>("\"${r.state.connectionState}\"")
if (callStatus == WebRTCCallStatus.Connected) { if (callStatus == WebRTCCallStatus.Connected) {
chatModel.activeCall.value = call.copy(callState = CallState.Connected) chatModel.activeCall.value = call.copy(callState = CallState.Connected)
setCallSound(call.soundSpeaker, audioViaBluetooth)
} }
withApi { chatModel.controller.apiCallStatus(call.contact, callStatus) } withApi { chatModel.controller.apiCallStatus(call.contact, callStatus) }
} catch (e: Error) { } catch (e: Error) {
@@ -108,8 +141,7 @@ fun ActiveCallView(chatModel: ChatModel) {
is WCallResponse.Connected -> { is WCallResponse.Connected -> {
chatModel.activeCall.value = call.copy(callState = CallState.Connected, connectionInfo = r.connectionInfo) chatModel.activeCall.value = call.copy(callState = CallState.Connected, connectionInfo = r.connectionInfo)
scope.launch { scope.launch {
delay(2000L) setCallSound(call.soundSpeaker, audioViaBluetooth)
setCallSound(cxt, call)
} }
} }
is WCallResponse.Ended -> { is WCallResponse.Ended -> {
@@ -143,15 +175,18 @@ fun ActiveCallView(chatModel: ChatModel) {
} }
} }
val call = chatModel.activeCall.value val call = chatModel.activeCall.value
if (call != null) ActiveCallOverlay(call, chatModel) if (call != null) ActiveCallOverlay(call, chatModel, audioViaBluetooth)
} }
val context = LocalContext.current val context = LocalContext.current
DisposableEffect(Unit) { DisposableEffect(Unit) {
val activity = context as? Activity ?: return@DisposableEffect onDispose {} val activity = context as? Activity ?: return@DisposableEffect onDispose {}
val prevVolumeControlStream = activity.volumeControlStream
activity.volumeControlStream = AudioManager.STREAM_VOICE_CALL
// Lock orientation to portrait in order to have good experience with calls // Lock orientation to portrait in order to have good experience with calls
activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
onDispose { onDispose {
activity.volumeControlStream = prevVolumeControlStream
// Unlock orientation // Unlock orientation
activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
} }
@@ -159,10 +194,10 @@ fun ActiveCallView(chatModel: ChatModel) {
} }
@Composable @Composable
private fun ActiveCallOverlay(call: Call, chatModel: ChatModel) { private fun ActiveCallOverlay(call: Call, chatModel: ChatModel, audioViaBluetooth: MutableState<Boolean>) {
var cxt = LocalContext.current
ActiveCallOverlayLayout( ActiveCallOverlayLayout(
call = call, call = call,
speakerCanBeEnabled = !audioViaBluetooth.value,
dismiss = { withApi { chatModel.callManager.endCall(call) } }, dismiss = { withApi { chatModel.callManager.endCall(call) } },
toggleAudio = { chatModel.callCommand.value = WCallCommand.Media(CallMediaType.Audio, enable = !call.audioEnabled) }, toggleAudio = { chatModel.callCommand.value = WCallCommand.Media(CallMediaType.Audio, enable = !call.audioEnabled) },
toggleVideo = { chatModel.callCommand.value = WCallCommand.Media(CallMediaType.Video, enable = !call.videoEnabled) }, toggleVideo = { chatModel.callCommand.value = WCallCommand.Media(CallMediaType.Video, enable = !call.videoEnabled) },
@@ -171,45 +206,62 @@ private fun ActiveCallOverlay(call: Call, chatModel: ChatModel) {
if (call != null) { if (call != null) {
call = call.copy(soundSpeaker = !call.soundSpeaker) call = call.copy(soundSpeaker = !call.soundSpeaker)
chatModel.activeCall.value = call chatModel.activeCall.value = call
setCallSound(cxt, call) setCallSound(call.soundSpeaker, audioViaBluetooth)
} }
}, },
flipCamera = { chatModel.callCommand.value = WCallCommand.Camera(call.localCamera.flipped) } flipCamera = { chatModel.callCommand.value = WCallCommand.Camera(call.localCamera.flipped) }
) )
} }
private fun setCallSound(cxt: Context, call: Call) { private fun setCallSound(speaker: Boolean, audioViaBluetooth: MutableState<Boolean>) {
Log.d(TAG, "setCallSound: set audio mode") val am = SimplexApp.context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
val am = cxt.getSystemService(Context.AUDIO_SERVICE) as AudioManager Log.d(TAG, "setCallSound: set audio mode, speaker enabled: $speaker")
if (call.soundSpeaker) { am.mode = AudioManager.MODE_IN_COMMUNICATION
am.mode = AudioManager.MODE_NORMAL if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
am.isSpeakerphoneOn = true val btDevice = am.availableCommunicationDevices.lastOrNull { it.type == AudioDeviceInfo.TYPE_BLUETOOTH_SCO }
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { val preferredSecondaryDevice = if (speaker) AudioDeviceInfo.TYPE_BUILTIN_SPEAKER else AudioDeviceInfo.TYPE_BUILTIN_EARPIECE
am.availableCommunicationDevices.firstOrNull { it.type == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER }?.let { if (btDevice != null) {
am.setCommunicationDevice(btDevice)
} else if (am.communicationDevice?.type != preferredSecondaryDevice) {
am.availableCommunicationDevices.firstOrNull { it.type == preferredSecondaryDevice }?.let {
am.setCommunicationDevice(it) am.setCommunicationDevice(it)
} }
} }
} else { } else {
am.mode = AudioManager.MODE_IN_CALL if (audioViaBluetooth.value) {
am.isSpeakerphoneOn = false am.isSpeakerphoneOn = false
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { am.startBluetoothSco()
am.availableCommunicationDevices.firstOrNull { it.type == AudioDeviceInfo.TYPE_BUILTIN_EARPIECE }?.let { } else {
am.setCommunicationDevice(it) am.stopBluetoothSco()
} am.isSpeakerphoneOn = speaker
} }
am.isBluetoothScoOn = am.isBluetoothScoAvailableOffCall && audioViaBluetooth.value
}
}
private fun dropAudioManagerOverrides() {
val am = SimplexApp.context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
am.mode = AudioManager.MODE_NORMAL
// Clear selected communication device to default value after we changed it in call
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
am.clearCommunicationDevice()
} else {
am.isSpeakerphoneOn = false
am.stopBluetoothSco()
} }
} }
@Composable @Composable
private fun ActiveCallOverlayLayout( private fun ActiveCallOverlayLayout(
call: Call, call: Call,
speakerCanBeEnabled: Boolean,
dismiss: () -> Unit, dismiss: () -> Unit,
toggleAudio: () -> Unit, toggleAudio: () -> Unit,
toggleVideo: () -> Unit, toggleVideo: () -> Unit,
toggleSound: () -> Unit, toggleSound: () -> Unit,
flipCamera: () -> Unit flipCamera: () -> Unit
) { ) {
Column(Modifier.padding(16.dp)) { Column(Modifier.padding(DEFAULT_PADDING)) {
when (call.peerMedia ?: call.localMedia) { when (call.peerMedia ?: call.localMedia) {
CallMediaType.Video -> { CallMediaType.Video -> {
CallInfoView(call, alignment = Alignment.Start) CallInfoView(call, alignment = Alignment.Start)
@@ -218,14 +270,14 @@ private fun ActiveCallOverlayLayout(
ToggleAudioButton(call, toggleAudio) ToggleAudioButton(call, toggleAudio)
Spacer(Modifier.size(40.dp)) Spacer(Modifier.size(40.dp))
IconButton(onClick = dismiss) { IconButton(onClick = dismiss) {
Icon(Icons.Filled.CallEnd, stringResource(R.string.icon_descr_hang_up), tint = Color.Red, modifier = Modifier.size(64.dp)) Icon(painterResource(R.drawable.ic_call_end_filled), stringResource(R.string.icon_descr_hang_up), tint = Color.Red, modifier = Modifier.size(64.dp))
} }
if (call.videoEnabled) { if (call.videoEnabled) {
ControlButton(call, Icons.Filled.FlipCameraAndroid, R.string.icon_descr_flip_camera, flipCamera) ControlButton(call, painterResource(R.drawable.ic_flip_camera_android_filled), R.string.icon_descr_flip_camera, flipCamera)
ControlButton(call, Icons.Filled.Videocam, R.string.icon_descr_video_off, toggleVideo) ControlButton(call, painterResource(R.drawable.ic_videocam_filled), R.string.icon_descr_video_off, toggleVideo)
} else { } else {
Spacer(Modifier.size(48.dp)) Spacer(Modifier.size(48.dp))
ControlButton(call, Icons.Outlined.VideocamOff, R.string.icon_descr_video_on, toggleVideo) ControlButton(call, painterResource(R.drawable.ic_videocam_off), R.string.icon_descr_video_on, toggleVideo)
} }
} }
} }
@@ -240,10 +292,10 @@ private fun ActiveCallOverlayLayout(
CallInfoView(call, alignment = Alignment.CenterHorizontally) CallInfoView(call, alignment = Alignment.CenterHorizontally)
} }
Spacer(Modifier.fillMaxHeight().weight(1f)) Spacer(Modifier.fillMaxHeight().weight(1f))
Box(Modifier.fillMaxWidth().padding(bottom = 48.dp), contentAlignment = Alignment.CenterStart) { Box(Modifier.fillMaxWidth().padding(bottom = DEFAULT_BOTTOM_PADDING), contentAlignment = Alignment.CenterStart) {
Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) { Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
IconButton(onClick = dismiss) { IconButton(onClick = dismiss) {
Icon(Icons.Filled.CallEnd, stringResource(R.string.icon_descr_hang_up), tint = Color.Red, modifier = Modifier.size(64.dp)) Icon(painterResource(R.drawable.ic_call_end_filled), stringResource(R.string.icon_descr_hang_up), tint = Color.Red, modifier = Modifier.size(64.dp))
} }
} }
Box(Modifier.padding(start = 32.dp)) { Box(Modifier.padding(start = 32.dp)) {
@@ -251,7 +303,7 @@ private fun ActiveCallOverlayLayout(
} }
Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.CenterEnd) { Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.CenterEnd) {
Box(Modifier.padding(end = 32.dp)) { Box(Modifier.padding(end = 32.dp)) {
ToggleSoundButton(call, toggleSound) ToggleSoundButton(call, speakerCanBeEnabled, toggleSound)
} }
} }
} }
@@ -261,10 +313,10 @@ private fun ActiveCallOverlayLayout(
} }
@Composable @Composable
private fun ControlButton(call: Call, icon: ImageVector, @StringRes iconText: Int, action: () -> Unit) { private fun ControlButton(call: Call, icon: Painter, @StringRes iconText: Int, action: () -> Unit, enabled: Boolean = true) {
if (call.hasMedia) { if (call.hasMedia) {
IconButton(onClick = action) { IconButton(onClick = action, enabled = enabled) {
Icon(icon, stringResource(iconText), tint = Color(0xFFFFFFD8), modifier = Modifier.size(40.dp)) Icon(icon, stringResource(iconText), tint = if (enabled) Color(0xFFFFFFD8) else MaterialTheme.colors.secondary, modifier = Modifier.size(40.dp))
} }
} else { } else {
Spacer(Modifier.size(40.dp)) Spacer(Modifier.size(40.dp))
@@ -274,18 +326,18 @@ private fun ControlButton(call: Call, icon: ImageVector, @StringRes iconText: In
@Composable @Composable
private fun ToggleAudioButton(call: Call, toggleAudio: () -> Unit) { private fun ToggleAudioButton(call: Call, toggleAudio: () -> Unit) {
if (call.audioEnabled) { if (call.audioEnabled) {
ControlButton(call, Icons.Outlined.Mic, R.string.icon_descr_audio_off, toggleAudio) ControlButton(call, painterResource(R.drawable.ic_mic), R.string.icon_descr_audio_off, toggleAudio)
} else { } else {
ControlButton(call, Icons.Outlined.MicOff, R.string.icon_descr_audio_on, toggleAudio) ControlButton(call, painterResource(R.drawable.ic_mic_off), R.string.icon_descr_audio_on, toggleAudio)
} }
} }
@Composable @Composable
private fun ToggleSoundButton(call: Call, toggleSound: () -> Unit) { private fun ToggleSoundButton(call: Call, enabled: Boolean, toggleSound: () -> Unit) {
if (call.soundSpeaker) { if (call.soundSpeaker) {
ControlButton(call, Icons.Outlined.VolumeUp, R.string.icon_descr_speaker_off, toggleSound) ControlButton(call, painterResource(R.drawable.ic_volume_up), R.string.icon_descr_speaker_off, toggleSound, enabled)
} else { } else {
ControlButton(call, Icons.Outlined.VolumeDown, R.string.icon_descr_speaker_on, toggleSound) ControlButton(call, painterResource(R.drawable.ic_volume_down), R.string.icon_descr_speaker_on, toggleSound, enabled)
} }
} }
@@ -297,10 +349,10 @@ fun CallInfoView(call: Call, alignment: Alignment.Horizontal) {
InfoText(call.contact.chatViewName, style = MaterialTheme.typography.h2) InfoText(call.contact.chatViewName, style = MaterialTheme.typography.h2)
InfoText(call.callState.text) InfoText(call.callState.text)
val connInfo = val connInfo = call.connectionInfo
if (call.connectionInfo == null) "" // val connInfoText = if (connInfo == null) "" else " (${connInfo.text}, ${connInfo.protocolText})"
else " (${call.connectionInfo.text})" val connInfoText = if (connInfo == null) "" else " (${connInfo.text})"
InfoText(call.encryptionStatus + connInfo) InfoText(call.encryptionStatus + connInfoText)
} }
} }
@@ -315,7 +367,7 @@ fun CallInfoView(call: Call, alignment: Alignment.Horizontal) {
// horizontalAlignment = Alignment.CenterHorizontally, // horizontalAlignment = Alignment.CenterHorizontally,
// verticalArrangement = Arrangement.spacedBy(12.dp), // verticalArrangement = Arrangement.spacedBy(12.dp),
// modifier = Modifier // modifier = Modifier
// .background(MaterialTheme.colors.background) // .themedBackground()
// .fillMaxSize() // .fillMaxSize()
// ) { // ) {
// WebRTCView(callCommand) { apiMsg -> // WebRTCView(callCommand) { apiMsg ->
@@ -480,8 +532,12 @@ fun PreviewActiveCallOverlayVideo() {
callState = CallState.Negotiated, callState = CallState.Negotiated,
localMedia = CallMediaType.Video, localMedia = CallMediaType.Video,
peerMedia = CallMediaType.Video, peerMedia = CallMediaType.Video,
connectionInfo = ConnectionInfo(RTCIceCandidate(RTCIceCandidateType.Host), RTCIceCandidate(RTCIceCandidateType.Host)) connectionInfo = ConnectionInfo(
RTCIceCandidate(RTCIceCandidateType.Host, "tcp", null),
RTCIceCandidate(RTCIceCandidateType.Host, "tcp", null)
)
), ),
speakerCanBeEnabled = true,
dismiss = {}, dismiss = {},
toggleAudio = {}, toggleAudio = {},
toggleVideo = {}, toggleVideo = {},
@@ -501,8 +557,12 @@ fun PreviewActiveCallOverlayAudio() {
callState = CallState.Negotiated, callState = CallState.Negotiated,
localMedia = CallMediaType.Audio, localMedia = CallMediaType.Audio,
peerMedia = CallMediaType.Audio, peerMedia = CallMediaType.Audio,
connectionInfo = ConnectionInfo(RTCIceCandidate(RTCIceCandidateType.Host), RTCIceCandidate(RTCIceCandidateType.Host)) connectionInfo = ConnectionInfo(
RTCIceCandidate(RTCIceCandidateType.Host, "udp", null),
RTCIceCandidate(RTCIceCandidateType.Host, "udp", null)
)
), ),
speakerCanBeEnabled = true,
dismiss = {}, dismiss = {},
toggleAudio = {}, toggleAudio = {},
toggleVideo = {}, toggleVideo = {},
@@ -12,19 +12,19 @@ import android.view.WindowManager
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.* import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.scale import androidx.compose.ui.draw.scale
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
@@ -36,7 +36,6 @@ import chat.simplex.app.model.*
import chat.simplex.app.model.NtfManager.Companion.OpenChatAction import chat.simplex.app.model.NtfManager.Companion.OpenChatAction
import chat.simplex.app.ui.theme.* import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.helpers.ProfileImage import chat.simplex.app.views.helpers.ProfileImage
import chat.simplex.app.views.onboarding.SimpleXLogo
import kotlinx.datetime.Clock import kotlinx.datetime.Clock
class IncomingCallActivity: ComponentActivity() { class IncomingCallActivity: ComponentActivity() {
@@ -98,7 +97,7 @@ fun IncomingCallActivityView(m: ChatModel) {
SimpleXTheme { SimpleXTheme {
Surface( Surface(
Modifier Modifier
.background(MaterialTheme.colors.background) .themedBackground()
.fillMaxSize()) { .fillMaxSize()) {
if (showCallView) { if (showCallView) {
Box { Box {
@@ -126,6 +125,7 @@ fun IncomingCallLockScreenAlert(invitation: RcvCallInvitation, chatModel: ChatMo
IncomingCallLockScreenAlertLayout( IncomingCallLockScreenAlertLayout(
invitation, invitation,
callOnLockScreen, callOnLockScreen,
chatModel,
rejectCall = { cm.endCall(invitation = invitation) }, rejectCall = { cm.endCall(invitation = invitation) },
ignoreCall = { ignoreCall = {
chatModel.activeCallInvitation.value = null chatModel.activeCallInvitation.value = null
@@ -135,6 +135,7 @@ fun IncomingCallLockScreenAlert(invitation: RcvCallInvitation, chatModel: ChatMo
openApp = { openApp = {
val intent = Intent(context, MainActivity::class.java) val intent = Intent(context, MainActivity::class.java)
.setAction(OpenChatAction) .setAction(OpenChatAction)
.putExtra("userId", invitation.user.userId)
.putExtra("chatId", invitation.contact.id) .putExtra("chatId", invitation.contact.id)
context.startActivity(intent) context.startActivity(intent)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
@@ -149,6 +150,7 @@ fun IncomingCallLockScreenAlert(invitation: RcvCallInvitation, chatModel: ChatMo
fun IncomingCallLockScreenAlertLayout( fun IncomingCallLockScreenAlertLayout(
invitation: RcvCallInvitation, invitation: RcvCallInvitation,
callOnLockScreen: CallOnLockScreen?, callOnLockScreen: CallOnLockScreen?,
chatModel: ChatModel,
rejectCall: () -> Unit, rejectCall: () -> Unit,
ignoreCall: () -> Unit, ignoreCall: () -> Unit,
acceptCall: () -> Unit, acceptCall: () -> Unit,
@@ -160,31 +162,42 @@ fun IncomingCallLockScreenAlertLayout(
.fillMaxSize(), .fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
IncomingCallInfo(invitation) IncomingCallInfo(invitation, chatModel)
Spacer(Modifier.fillMaxHeight().weight(1f)) Spacer(Modifier.fillMaxHeight().weight(1f))
if (callOnLockScreen == CallOnLockScreen.ACCEPT) { if (callOnLockScreen == CallOnLockScreen.ACCEPT) {
ProfileImage(size = 192.dp, image = invitation.contact.profile.image) ProfileImage(size = 192.dp, image = invitation.contact.profile.image)
Text(invitation.contact.chatViewName, style = MaterialTheme.typography.h2) Text(invitation.contact.chatViewName, style = MaterialTheme.typography.h2)
Spacer(Modifier.fillMaxHeight().weight(1f)) Spacer(Modifier.fillMaxHeight().weight(1f))
Row { Row {
LockScreenCallButton(stringResource(R.string.reject), Icons.Filled.CallEnd, Color.Red, rejectCall) LockScreenCallButton(stringResource(R.string.reject), painterResource(R.drawable.ic_call_end_filled), Color.Red, rejectCall)
Spacer(Modifier.size(48.dp)) Spacer(Modifier.size(48.dp))
LockScreenCallButton(stringResource(R.string.ignore), Icons.Filled.Close, MaterialTheme.colors.primary, ignoreCall) LockScreenCallButton(stringResource(R.string.ignore), painterResource(R.drawable.ic_close), MaterialTheme.colors.primary, ignoreCall)
Spacer(Modifier.size(48.dp)) Spacer(Modifier.size(48.dp))
LockScreenCallButton(stringResource(R.string.accept), Icons.Filled.Check, SimplexGreen, acceptCall) LockScreenCallButton(stringResource(R.string.accept), painterResource(R.drawable.ic_check_filled), SimplexGreen, acceptCall)
} }
} else if (callOnLockScreen == CallOnLockScreen.SHOW) { } else if (callOnLockScreen == CallOnLockScreen.SHOW) {
SimpleXLogo() SimpleXLogo()
Text(stringResource(R.string.open_simplex_chat_to_accept_call), textAlign = TextAlign.Center, lineHeight = 22.sp) Text(stringResource(R.string.open_simplex_chat_to_accept_call), textAlign = TextAlign.Center, lineHeight = 22.sp)
Text(stringResource(R.string.allow_accepting_calls_from_lock_screen), textAlign = TextAlign.Center, style = MaterialTheme.typography.body2, lineHeight = 22.sp) Text(stringResource(R.string.allow_accepting_calls_from_lock_screen), textAlign = TextAlign.Center, style = MaterialTheme.typography.body2, lineHeight = 22.sp)
Spacer(Modifier.fillMaxHeight().weight(1f)) Spacer(Modifier.fillMaxHeight().weight(1f))
SimpleButton(text = stringResource(R.string.open_verb), icon = Icons.Filled.Check, click = openApp) SimpleButton(text = stringResource(R.string.open_verb), icon = painterResource(R.drawable.ic_check_filled), click = openApp)
} }
} }
} }
@Composable @Composable
private fun LockScreenCallButton(text: String, icon: ImageVector, color: Color, action: () -> Unit) { private fun SimpleXLogo() {
Image(
painter = painterResource(if (isInDarkTheme()) R.drawable.logo_light else R.drawable.logo),
contentDescription = stringResource(R.string.image_descr_simplex_logo),
modifier = Modifier
.padding(vertical = DEFAULT_PADDING)
.fillMaxWidth(0.80f)
)
}
@Composable
private fun LockScreenCallButton(text: String, icon: Painter, color: Color, action: () -> Unit) {
Surface( Surface(
shape = RoundedCornerShape(10.dp), shape = RoundedCornerShape(10.dp),
color = Color.Transparent color = Color.Transparent
@@ -198,8 +211,8 @@ private fun LockScreenCallButton(text: String, icon: ImageVector, color: Color,
IconButton(action) { IconButton(action) {
Icon(icon, text, tint = color, modifier = Modifier.scale(1.75f)) Icon(icon, text, tint = color, modifier = Modifier.scale(1.75f))
} }
Spacer(Modifier.height(16.dp)) Spacer(Modifier.height(DEFAULT_PADDING))
Text(text, style = MaterialTheme.typography.body2, color = HighOrLowlight) Text(text, style = MaterialTheme.typography.body2, color = MaterialTheme.colors.secondary)
} }
} }
} }
@@ -213,16 +226,18 @@ fun PreviewIncomingCallLockScreenAlert() {
SimpleXTheme(true) { SimpleXTheme(true) {
Surface( Surface(
Modifier Modifier
.background(MaterialTheme.colors.background) .themedBackground()
.fillMaxSize()) { .fillMaxSize()) {
IncomingCallLockScreenAlertLayout( IncomingCallLockScreenAlertLayout(
invitation = RcvCallInvitation( invitation = RcvCallInvitation(
user = User.sampleData,
contact = Contact.sampleData, contact = Contact.sampleData,
callType = CallType(media = CallMediaType.Audio, capabilities = CallCapabilities(encryption = false)), callType = CallType(media = CallMediaType.Audio, capabilities = CallCapabilities(encryption = false)),
sharedKey = null, sharedKey = null,
callTs = Clock.System.now() callTs = Clock.System.now()
), ),
callOnLockScreen = null, callOnLockScreen = null,
chatModel = SimplexApp.context.chatModel,
rejectCall = {}, rejectCall = {},
ignoreCall = {}, ignoreCall = {},
acceptCall = {}, acceptCall = {},
@@ -4,22 +4,22 @@ import androidx.compose.foundation.*
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.* import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.scale import androidx.compose.ui.draw.scale
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import chat.simplex.app.R import chat.simplex.app.R
import chat.simplex.app.model.ChatModel import chat.simplex.app.SimplexApp
import chat.simplex.app.model.Contact import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.* import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.helpers.ProfileImage
import chat.simplex.app.views.usersettings.ProfilePreview import chat.simplex.app.views.usersettings.ProfilePreview
import kotlinx.datetime.Clock import kotlinx.datetime.Clock
@@ -32,6 +32,7 @@ fun IncomingCallAlertView(invitation: RcvCallInvitation, chatModel: ChatModel) {
DisposableEffect(true) { onDispose { SoundPlayer.shared.stop() } } DisposableEffect(true) { onDispose { SoundPlayer.shared.stop() } }
IncomingCallAlertLayout( IncomingCallAlertLayout(
invitation, invitation,
chatModel,
rejectCall = { cm.endCall(invitation = invitation) }, rejectCall = { cm.endCall(invitation = invitation) },
ignoreCall = { ignoreCall = {
chatModel.activeCallInvitation.value = null chatModel.activeCallInvitation.value = null
@@ -44,40 +45,45 @@ fun IncomingCallAlertView(invitation: RcvCallInvitation, chatModel: ChatModel) {
@Composable @Composable
fun IncomingCallAlertLayout( fun IncomingCallAlertLayout(
invitation: RcvCallInvitation, invitation: RcvCallInvitation,
chatModel: ChatModel,
rejectCall: () -> Unit, rejectCall: () -> Unit,
ignoreCall: () -> Unit, ignoreCall: () -> Unit,
acceptCall: () -> Unit acceptCall: () -> Unit
) { ) {
val color = if (isInDarkTheme()) IncomingCallDark else IncomingCallLight val color = if (isInDarkTheme()) IncomingCallDark else IncomingCallLight
Column(Modifier.fillMaxWidth().background(color).padding(top = 16.dp, bottom = 16.dp, start = 16.dp, end = 8.dp)) { Column(Modifier.fillMaxWidth().background(color).padding(top = DEFAULT_PADDING, bottom = DEFAULT_PADDING, start = DEFAULT_PADDING, end = 8.dp)) {
IncomingCallInfo(invitation) IncomingCallInfo(invitation, chatModel)
Spacer(Modifier.height(8.dp)) Spacer(Modifier.height(8.dp))
Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween) { Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween) {
Row(Modifier.fillMaxWidth().weight(1f), verticalAlignment = Alignment.CenterVertically) { Row(Modifier.fillMaxWidth().weight(1f), verticalAlignment = Alignment.CenterVertically) {
ProfilePreview(profileOf = invitation.contact, size = 64.dp, color = Color.White) ProfilePreview(profileOf = invitation.contact, size = 64.dp, color = Color.White)
} }
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
CallButton(stringResource(R.string.reject), Icons.Filled.CallEnd, Color.Red, rejectCall) CallButton(stringResource(R.string.reject), painterResource(R.drawable.ic_call_end_filled), Color.Red, rejectCall)
CallButton(stringResource(R.string.ignore), Icons.Filled.Close, MaterialTheme.colors.primary, ignoreCall) CallButton(stringResource(R.string.ignore), painterResource(R.drawable.ic_close), MaterialTheme.colors.primary, ignoreCall)
CallButton(stringResource(R.string.accept), Icons.Filled.Check, SimplexGreen, acceptCall) CallButton(stringResource(R.string.accept), painterResource(R.drawable.ic_check_filled), SimplexGreen, acceptCall)
} }
} }
} }
} }
@Composable @Composable
fun IncomingCallInfo(invitation: RcvCallInvitation) { fun IncomingCallInfo(invitation: RcvCallInvitation, chatModel: ChatModel) {
@Composable fun CallIcon(icon: ImageVector, descr: String) = Icon(icon, descr, tint = SimplexGreen) @Composable fun CallIcon(icon: Painter, descr: String) = Icon(icon, descr, tint = SimplexGreen)
Row { Row(verticalAlignment = Alignment.CenterVertically) {
if (invitation.callType.media == CallMediaType.Video) CallIcon(Icons.Filled.Videocam, stringResource(R.string.icon_descr_video_call)) if (chatModel.users.size > 1) {
else CallIcon(Icons.Filled.Phone, stringResource(R.string.icon_descr_audio_call)) ProfileImage(size = 32.dp, image = invitation.user.profile.image, color = MaterialTheme.colors.secondaryVariant)
Spacer(Modifier.width(4.dp))
}
if (invitation.callType.media == CallMediaType.Video) CallIcon(painterResource(R.drawable.ic_videocam_filled), stringResource(R.string.icon_descr_video_call))
else CallIcon(painterResource(R.drawable.ic_call_filled), stringResource(R.string.icon_descr_audio_call))
Spacer(Modifier.width(4.dp)) Spacer(Modifier.width(4.dp))
Text(invitation.callTypeText) Text(invitation.callTypeText)
} }
} }
@Composable @Composable
private fun CallButton(text: String, icon: ImageVector, color: Color, action: () -> Unit) { private fun CallButton(text: String, icon: Painter, color: Color, action: () -> Unit) {
Surface( Surface(
shape = RoundedCornerShape(10.dp), shape = RoundedCornerShape(10.dp),
color = Color.Transparent color = Color.Transparent
@@ -90,7 +96,7 @@ private fun CallButton(text: String, icon: ImageVector, color: Color, action: ()
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
Icon(icon, text, tint = color, modifier = Modifier.scale(1.2f)) Icon(icon, text, tint = color, modifier = Modifier.scale(1.2f))
Text(text, style = MaterialTheme.typography.body2, color = HighOrLowlight) Text(text, style = MaterialTheme.typography.body2, color = MaterialTheme.colors.secondary)
} }
} }
} }
@@ -101,11 +107,13 @@ fun PreviewIncomingCallAlertLayout() {
SimpleXTheme { SimpleXTheme {
IncomingCallAlertLayout( IncomingCallAlertLayout(
invitation = RcvCallInvitation( invitation = RcvCallInvitation(
user = User.sampleData,
contact = Contact.sampleData, contact = Contact.sampleData,
callType = CallType(media = CallMediaType.Audio, capabilities = CallCapabilities(encryption = false)), callType = CallType(media = CallMediaType.Audio, capabilities = CallCapabilities(encryption = false)),
sharedKey = null, sharedKey = null,
callTs = Clock.System.now() callTs = Clock.System.now()
), ),
chatModel = SimplexApp.context.chatModel,
rejectCall = {}, rejectCall = {},
ignoreCall = {}, ignoreCall = {},
acceptCall = {} acceptCall = {}
@@ -48,4 +48,4 @@ class SoundPlayer {
companion object { companion object {
val shared = SoundPlayer() val shared = SoundPlayer()
} }
} }
@@ -1,15 +1,19 @@
package chat.simplex.app.views.call package chat.simplex.app.views.call
import android.util.Log
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import chat.simplex.app.R import androidx.compose.ui.text.toUpperCase
import chat.simplex.app.SimplexApp import chat.simplex.app.*
import chat.simplex.app.model.Contact import chat.simplex.app.model.Contact
import chat.simplex.app.model.User
import chat.simplex.app.views.helpers.generalGetString import chat.simplex.app.views.helpers.generalGetString
import kotlinx.datetime.Instant import kotlinx.datetime.Instant
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import java.net.URI import java.net.URI
import java.util.*
import kotlin.collections.ArrayList
data class Call( data class Call(
val contact: Contact, val contact: Contact,
@@ -61,39 +65,39 @@ enum class CallState {
} }
} }
@Serializable class WVAPICall(val corrId: Int? = null, val command: WCallCommand) @Serializable data class WVAPICall(val corrId: Int? = null, val command: WCallCommand)
@Serializable class WVAPIMessage(val corrId: Int? = null, val resp: WCallResponse, val command: WCallCommand? = null) @Serializable data class WVAPIMessage(val corrId: Int? = null, val resp: WCallResponse, val command: WCallCommand? = null)
@Serializable @Serializable
sealed class WCallCommand { sealed class WCallCommand {
@Serializable @SerialName("capabilities") object Capabilities: WCallCommand() @Serializable @SerialName("capabilities") object Capabilities: WCallCommand()
@Serializable @SerialName("start") class Start(val media: CallMediaType, val aesKey: String? = null, val iceServers: List<RTCIceServer>? = null, val relay: Boolean? = null): WCallCommand() @Serializable @SerialName("start") data class Start(val media: CallMediaType, val aesKey: String? = null, val iceServers: List<RTCIceServer>? = null, val relay: Boolean? = null): WCallCommand()
@Serializable @SerialName("offer") class Offer(val offer: String, val iceCandidates: String, val media: CallMediaType, val aesKey: String? = null, val iceServers: List<RTCIceServer>? = null, val relay: Boolean? = null): WCallCommand() @Serializable @SerialName("offer") data class Offer(val offer: String, val iceCandidates: String, val media: CallMediaType, val aesKey: String? = null, val iceServers: List<RTCIceServer>? = null, val relay: Boolean? = null): WCallCommand()
@Serializable @SerialName("answer") class Answer (val answer: String, val iceCandidates: String): WCallCommand() @Serializable @SerialName("answer") data class Answer (val answer: String, val iceCandidates: String): WCallCommand()
@Serializable @SerialName("ice") class Ice(val iceCandidates: String): WCallCommand() @Serializable @SerialName("ice") data class Ice(val iceCandidates: String): WCallCommand()
@Serializable @SerialName("media") class Media(val media: CallMediaType, val enable: Boolean): WCallCommand() @Serializable @SerialName("media") data class Media(val media: CallMediaType, val enable: Boolean): WCallCommand()
@Serializable @SerialName("camera") class Camera(val camera: VideoCamera): WCallCommand() @Serializable @SerialName("camera") data class Camera(val camera: VideoCamera): WCallCommand()
@Serializable @SerialName("end") object End: WCallCommand() @Serializable @SerialName("end") object End: WCallCommand()
} }
@Serializable @Serializable
sealed class WCallResponse { sealed class WCallResponse {
@Serializable @SerialName("capabilities") class Capabilities(val capabilities: CallCapabilities): WCallResponse() @Serializable @SerialName("capabilities") data class Capabilities(val capabilities: CallCapabilities): WCallResponse()
@Serializable @SerialName("offer") class Offer(val offer: String, val iceCandidates: String, val capabilities: CallCapabilities): WCallResponse() @Serializable @SerialName("offer") data class Offer(val offer: String, val iceCandidates: String, val capabilities: CallCapabilities): WCallResponse()
@Serializable @SerialName("answer") class Answer(val answer: String, val iceCandidates: String): WCallResponse() @Serializable @SerialName("answer") data class Answer(val answer: String, val iceCandidates: String): WCallResponse()
@Serializable @SerialName("ice") class Ice(val iceCandidates: String): WCallResponse() @Serializable @SerialName("ice") data class Ice(val iceCandidates: String): WCallResponse()
@Serializable @SerialName("connection") class Connection(val state: ConnectionState): WCallResponse() @Serializable @SerialName("connection") data class Connection(val state: ConnectionState): WCallResponse()
@Serializable @SerialName("connected") class Connected(val connectionInfo: ConnectionInfo): WCallResponse() @Serializable @SerialName("connected") data class Connected(val connectionInfo: ConnectionInfo): WCallResponse()
@Serializable @SerialName("ended") object Ended: WCallResponse() @Serializable @SerialName("ended") object Ended: WCallResponse()
@Serializable @SerialName("ok") object Ok: WCallResponse() @Serializable @SerialName("ok") object Ok: WCallResponse()
@Serializable @SerialName("error") class Error(val message: String): WCallResponse() @Serializable @SerialName("error") data class Error(val message: String): WCallResponse()
} }
@Serializable class WebRTCCallOffer(val callType: CallType, val rtcSession: WebRTCSession) @Serializable data class WebRTCCallOffer(val callType: CallType, val rtcSession: WebRTCSession)
@Serializable class WebRTCSession(val rtcSession: String, val rtcIceCandidates: String) @Serializable data class WebRTCSession(val rtcSession: String, val rtcIceCandidates: String)
@Serializable class WebRTCExtraInfo(val rtcIceCandidates: String) @Serializable data class WebRTCExtraInfo(val rtcIceCandidates: String)
@Serializable class CallType(val media: CallMediaType, val capabilities: CallCapabilities) @Serializable data class CallType(val media: CallMediaType, val capabilities: CallCapabilities)
@Serializable class RcvCallInvitation(val contact: Contact, val callType: CallType, val sharedKey: String? = null, val callTs: Instant) { @Serializable data class RcvCallInvitation(val user: User, val contact: Contact, val callType: CallType, val sharedKey: String? = null, val callTs: Instant) {
val callTypeText: String get() = generalGetString(when(callType.media) { val callTypeText: String get() = generalGetString(when(callType.media) {
CallMediaType.Video -> if (sharedKey == null) R.string.video_call_no_encryption else R.string.encrypted_video_call CallMediaType.Video -> if (sharedKey == null) R.string.video_call_no_encryption else R.string.encrypted_video_call
CallMediaType.Audio -> if (sharedKey == null) R.string.audio_call_no_encryption else R.string.encrypted_audio_call CallMediaType.Audio -> if (sharedKey == null) R.string.audio_call_no_encryption else R.string.encrypted_audio_call
@@ -103,19 +107,32 @@ sealed class WCallResponse {
CallMediaType.Audio -> R.string.incoming_audio_call CallMediaType.Audio -> R.string.incoming_audio_call
}) })
} }
@Serializable class CallCapabilities(val encryption: Boolean) @Serializable data class CallCapabilities(val encryption: Boolean)
@Serializable class ConnectionInfo(private val localCandidate: RTCIceCandidate?, private val remoteCandidate: RTCIceCandidate?) { @Serializable data class ConnectionInfo(private val localCandidate: RTCIceCandidate?, private val remoteCandidate: RTCIceCandidate?) {
val text: String @Composable get() = when { val text: String @Composable get() {
localCandidate?.candidateType == RTCIceCandidateType.Host && remoteCandidate?.candidateType == RTCIceCandidateType.Host -> val local = localCandidate?.candidateType
stringResource(R.string.call_connection_peer_to_peer) val remote = remoteCandidate?.candidateType
localCandidate?.candidateType == RTCIceCandidateType.Relay && remoteCandidate?.candidateType == RTCIceCandidateType.Relay -> return when {
stringResource(R.string.call_connection_via_relay) local == RTCIceCandidateType.Host && remote == RTCIceCandidateType.Host ->
else -> stringResource(R.string.call_connection_peer_to_peer)
"${localCandidate?.candidateType?.value ?: "unknown"} / ${remoteCandidate?.candidateType?.value ?: "unknown"}" local == RTCIceCandidateType.Relay && remote == RTCIceCandidateType.Relay ->
stringResource(R.string.call_connection_via_relay)
else ->
"${local?.value ?: "unknown"} / ${remote?.value ?: "unknown"}"
}
}
val protocolText: String get() {
val local = localCandidate?.protocol?.uppercase(Locale.ROOT) ?: "unknown"
val localRelay = localCandidate?.relayProtocol?.uppercase(Locale.ROOT) ?: "unknown"
val remote = remoteCandidate?.protocol?.uppercase(Locale.ROOT) ?: "unknown"
val localText = if (localRelay == local || localCandidate?.relayProtocol == null) local else "$local ($localRelay)"
return if (local == remote) localText else "$localText / $remote"
} }
} }
// https://developer.mozilla.org/en-US/docs/Web/API/RTCIceCandidate // https://developer.mozilla.org/en-US/docs/Web/API/RTCIceCandidate
@Serializable class RTCIceCandidate(val candidateType: RTCIceCandidateType?) @Serializable data class RTCIceCandidate(val candidateType: RTCIceCandidateType?, val protocol: String?, val relayProtocol: String?)
// https://developer.mozilla.org/en-US/docs/Web/API/RTCIceServer // https://developer.mozilla.org/en-US/docs/Web/API/RTCIceServer
@Serializable data class RTCIceServer(val urls: List<String>, val username: String? = null, val credential: String? = null) @Serializable data class RTCIceServer(val urls: List<String>, val username: String? = null, val credential: String? = null)
@@ -150,7 +167,7 @@ enum class VideoCamera {
} }
@Serializable @Serializable
class ConnectionState( data class ConnectionState(
val connectionState: String, val connectionState: String,
val iceConnectionState: String, val iceConnectionState: String,
val iceGatheringState: String, val iceGatheringState: String,
@@ -158,20 +175,22 @@ class ConnectionState(
) )
// the servers are expected in this format: // the servers are expected in this format:
// stun:stun.simplex.im:443 // stun:stun.simplex.im:443?transport=tcp
// turn:private:yleob6AVkiNI87hpR94Z@turn.simplex.im:443 // turn:private:yleob6AVkiNI87hpR94Z@turn.simplex.im:443?transport=tcp
fun parseRTCIceServer(str: String): RTCIceServer? { fun parseRTCIceServer(str: String): RTCIceServer? {
var s = replaceScheme(str, "stun:") var s = replaceScheme(str, "stun:")
s = replaceScheme(s, "turn:") s = replaceScheme(s, "turn:")
s = replaceScheme(s, "turns:")
val u = runCatching { URI(s) }.getOrNull() val u = runCatching { URI(s) }.getOrNull()
if (u != null) { if (u != null) {
val scheme = u.scheme val scheme = u.scheme
val host = u.host val host = u.host
val port = u.port val port = u.port
if (u.path == "" && (scheme == "stun" || scheme == "turn")) { if (u.path == "" && (scheme == "stun" || scheme == "turn" || scheme == "turns")) {
val userInfo = u.userInfo?.split(":") val userInfo = u.userInfo?.split(":")
val query = if (u.query == null || u.query == "") "" else "?${u.query}"
return RTCIceServer( return RTCIceServer(
urls = listOf("$scheme:$host:$port"), urls = listOf("$scheme:$host:$port$query"),
username = userInfo?.getOrNull(0), username = userInfo?.getOrNull(0),
credential = userInfo?.getOrNull(1) credential = userInfo?.getOrNull(1)
) )
@@ -2,7 +2,8 @@ package chat.simplex.app.views.chat
import InfoRow import InfoRow
import InfoRowEllipsis import InfoRowEllipsis
import SectionDivider import SectionBottomSpacer
import SectionDividerSpaced
import SectionItemView import SectionItemView
import SectionSpacer import SectionSpacer
import SectionView import SectionView
@@ -12,9 +13,6 @@ import androidx.compose.foundation.*
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.material.* import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material.icons.outlined.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
@@ -22,6 +20,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.ClipboardManager import androidx.compose.ui.platform.ClipboardManager
import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
@@ -54,10 +53,14 @@ fun ChatInfoView(
val chat = chatModel.chats.firstOrNull { it.id == chatModel.chatId.value } val chat = chatModel.chats.firstOrNull { it.id == chatModel.chatId.value }
val developerTools = chatModel.controller.appPrefs.developerTools.get() val developerTools = chatModel.controller.appPrefs.developerTools.get()
if (chat != null) { if (chat != null) {
val contactNetworkStatus = remember(chatModel.networkStatuses.toMap()) {
mutableStateOf(chatModel.contactNetworkStatus(contact))
}
ChatInfoLayout( ChatInfoLayout(
chat, chat,
contact, contact,
connStats, connStats,
contactNetworkStatus.value,
customUserProfile, customUserProfile,
localAlias, localAlias,
connectionCode, connectionCode,
@@ -108,7 +111,7 @@ fun ChatInfoView(
} }
fun deleteContactDialog(chatInfo: ChatInfo, chatModel: ChatModel, close: (() -> Unit)? = null) { fun deleteContactDialog(chatInfo: ChatInfo, chatModel: ChatModel, close: (() -> Unit)? = null) {
AlertManager.shared.showAlertMsg( AlertManager.shared.showAlertDialog(
title = generalGetString(R.string.delete_contact_question), title = generalGetString(R.string.delete_contact_question),
text = generalGetString(R.string.delete_contact_all_messages_deleted_cannot_undo_warning), text = generalGetString(R.string.delete_contact_all_messages_deleted_cannot_undo_warning),
confirmText = generalGetString(R.string.delete_verb), confirmText = generalGetString(R.string.delete_verb),
@@ -122,12 +125,13 @@ fun deleteContactDialog(chatInfo: ChatInfo, chatModel: ChatModel, close: (() ->
close?.invoke() close?.invoke()
} }
} }
} },
destructive = true,
) )
} }
fun clearChatDialog(chatInfo: ChatInfo, chatModel: ChatModel, close: (() -> Unit)? = null) { fun clearChatDialog(chatInfo: ChatInfo, chatModel: ChatModel, close: (() -> Unit)? = null) {
AlertManager.shared.showAlertMsg( AlertManager.shared.showAlertDialog(
title = generalGetString(R.string.clear_chat_question), title = generalGetString(R.string.clear_chat_question),
text = generalGetString(R.string.clear_chat_warning), text = generalGetString(R.string.clear_chat_warning),
confirmText = generalGetString(R.string.clear_verb), confirmText = generalGetString(R.string.clear_verb),
@@ -140,7 +144,8 @@ fun clearChatDialog(chatInfo: ChatInfo, chatModel: ChatModel, close: (() -> Unit
close?.invoke() close?.invoke()
} }
} }
} },
destructive = true,
) )
} }
@@ -149,6 +154,7 @@ fun ChatInfoLayout(
chat: Chat, chat: Chat,
contact: Contact, contact: Contact,
connStats: ConnectionStats?, connStats: ConnectionStats?,
contactNetworkStatus: NetworkStatus,
customUserProfile: Profile?, customUserProfile: Profile?,
localAlias: String, localAlias: String,
connectionCode: String?, connectionCode: String?,
@@ -163,8 +169,7 @@ fun ChatInfoLayout(
Column( Column(
Modifier Modifier
.fillMaxWidth() .fillMaxWidth()
.verticalScroll(rememberScrollState()), .verticalScroll(rememberScrollState())
horizontalAlignment = Alignment.Start
) { ) {
Row( Row(
Modifier.fillMaxWidth(), Modifier.fillMaxWidth(),
@@ -174,64 +179,57 @@ fun ChatInfoLayout(
} }
LocalAliasEditor(localAlias, updateValue = onLocalAliasChanged) LocalAliasEditor(localAlias, updateValue = onLocalAliasChanged)
SectionSpacer()
if (customUserProfile != null) { if (customUserProfile != null) {
SectionSpacer()
SectionView(generalGetString(R.string.incognito).uppercase()) { SectionView(generalGetString(R.string.incognito).uppercase()) {
InfoRow(generalGetString(R.string.incognito_random_profile), customUserProfile.chatViewName) InfoRow(generalGetString(R.string.incognito_random_profile), customUserProfile.chatViewName)
} }
SectionDividerSpaced()
} }
SectionSpacer()
SectionView { SectionView {
if (connectionCode != null) { if (connectionCode != null) {
VerifyCodeButton(contact.verified, verifyClicked) VerifyCodeButton(contact.verified, verifyClicked)
SectionDivider()
} }
ContactPreferencesButton(openPreferences) ContactPreferencesButton(openPreferences)
} }
SectionSpacer() SectionDividerSpaced()
SectionView(title = stringResource(R.string.conn_stats_section_title_servers)) { SectionView(title = stringResource(R.string.conn_stats_section_title_servers)) {
SwitchAddressButton(switchContactAddress) SwitchAddressButton(switchContactAddress)
SectionDivider()
if (connStats != null) { if (connStats != null) {
SectionItemView({ SectionItemView({
AlertManager.shared.showAlertMsg( AlertManager.shared.showAlertMsg(
generalGetString(R.string.network_status), generalGetString(R.string.network_status),
chat.serverInfo.networkStatus.statusExplanation contactNetworkStatus.statusExplanation
)}) { )}) {
NetworkStatusRow(chat.serverInfo.networkStatus) NetworkStatusRow(contactNetworkStatus)
} }
val rcvServers = connStats.rcvServers val rcvServers = connStats.rcvServers
if (rcvServers != null && rcvServers.isNotEmpty()) { if (rcvServers != null && rcvServers.isNotEmpty()) {
SectionDivider()
SimplexServers(stringResource(R.string.receiving_via), rcvServers) SimplexServers(stringResource(R.string.receiving_via), rcvServers)
} }
val sndServers = connStats.sndServers val sndServers = connStats.sndServers
if (sndServers != null && sndServers.isNotEmpty()) { if (sndServers != null && sndServers.isNotEmpty()) {
SectionDivider()
SimplexServers(stringResource(R.string.sending_via), sndServers) SimplexServers(stringResource(R.string.sending_via), sndServers)
} }
} }
} }
SectionSpacer() SectionDividerSpaced()
SectionView { SectionView {
ClearChatButton(clearChat) ClearChatButton(clearChat)
SectionDivider()
DeleteContactButton(deleteContact) DeleteContactButton(deleteContact)
} }
SectionSpacer()
if (developerTools) { if (developerTools) {
SectionDividerSpaced()
SectionView(title = stringResource(R.string.section_title_for_console)) { SectionView(title = stringResource(R.string.section_title_for_console)) {
InfoRow(stringResource(R.string.info_row_local_name), chat.chatInfo.localDisplayName) InfoRow(stringResource(R.string.info_row_local_name), chat.chatInfo.localDisplayName)
SectionDivider()
InfoRow(stringResource(R.string.info_row_database_id), chat.chatInfo.apiId.toString()) InfoRow(stringResource(R.string.info_row_database_id), chat.chatInfo.apiId.toString())
} }
SectionSpacer()
} }
SectionBottomSpacer()
} }
} }
@@ -244,7 +242,7 @@ fun ChatInfoHeader(cInfo: ChatInfo, contact: Contact) {
ChatInfoImage(cInfo, size = 192.dp, iconColor = if (isInDarkTheme()) GroupDark else SettingsSecondaryLight) ChatInfoImage(cInfo, size = 192.dp, iconColor = if (isInDarkTheme()) GroupDark else SettingsSecondaryLight)
Row(Modifier.padding(bottom = 8.dp), verticalAlignment = Alignment.CenterVertically) { Row(Modifier.padding(bottom = 8.dp), verticalAlignment = Alignment.CenterVertically) {
if (contact.verified) { if (contact.verified) {
Icon(Icons.Outlined.VerifiedUser, null, Modifier.padding(end = 6.dp, top = 4.dp).size(24.dp), tint = HighOrLowlight) Icon(painterResource(R.drawable.ic_verified_user), null, Modifier.padding(end = 6.dp, top = 4.dp).size(24.dp), tint = MaterialTheme.colors.secondary)
} }
Text( Text(
contact.profile.displayName, style = MaterialTheme.typography.h1.copy(fontWeight = FontWeight.Normal), contact.profile.displayName, style = MaterialTheme.typography.h1.copy(fontWeight = FontWeight.Normal),
@@ -285,13 +283,13 @@ fun LocalAliasEditor(
Text( Text(
generalGetString(R.string.text_field_set_contact_placeholder), generalGetString(R.string.text_field_set_contact_placeholder),
textAlign = if (center) TextAlign.Center else TextAlign.Start, textAlign = if (center) TextAlign.Center else TextAlign.Start,
color = HighOrLowlight color = MaterialTheme.colors.secondary
) )
}, },
leadingIcon = if (leadingIcon) { leadingIcon = if (leadingIcon) {
{ Icon(Icons.Default.Edit, null, Modifier.padding(start = 7.dp)) } { Icon(painterResource(R.drawable.ic_edit_filled), null, Modifier.padding(start = 7.dp)) }
} else null, } else null,
color = HighOrLowlight, color = MaterialTheme.colors.secondary,
focus = focus, focus = focus,
textStyle = TextStyle.Default.copy(textAlign = if (value.isEmpty() || !center) TextAlign.Start else TextAlign.Center), textStyle = TextStyle.Default.copy(textAlign = if (value.isEmpty() || !center) TextAlign.Start else TextAlign.Center),
keyboardActions = KeyboardActions(onDone = { updateValue(value) }) keyboardActions = KeyboardActions(onDone = { updateValue(value) })
@@ -314,7 +312,7 @@ fun LocalAliasEditor(
} }
@Composable @Composable
private fun NetworkStatusRow(networkStatus: Chat.NetworkStatus) { private fun NetworkStatusRow(networkStatus: NetworkStatus) {
Row( Row(
Modifier.fillMaxSize(), Modifier.fillMaxSize(),
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
@@ -326,7 +324,7 @@ private fun NetworkStatusRow(networkStatus: Chat.NetworkStatus) {
) { ) {
Text(stringResource(R.string.network_status)) Text(stringResource(R.string.network_status))
Icon( Icon(
Icons.Outlined.Info, painterResource(R.drawable.ic_info),
stringResource(R.string.network_status), stringResource(R.string.network_status),
tint = MaterialTheme.colors.primary tint = MaterialTheme.colors.primary
) )
@@ -338,7 +336,7 @@ private fun NetworkStatusRow(networkStatus: Chat.NetworkStatus) {
) { ) {
Text( Text(
networkStatus.statusString, networkStatus.statusString,
color = HighOrLowlight color = MaterialTheme.colors.secondary
) )
ServerImage(networkStatus) ServerImage(networkStatus)
} }
@@ -346,16 +344,16 @@ private fun NetworkStatusRow(networkStatus: Chat.NetworkStatus) {
} }
@Composable @Composable
private fun ServerImage(networkStatus: Chat.NetworkStatus) { private fun ServerImage(networkStatus: NetworkStatus) {
Box(Modifier.size(18.dp)) { Box(Modifier.size(18.dp)) {
when (networkStatus) { when (networkStatus) {
is Chat.NetworkStatus.Connected -> is NetworkStatus.Connected ->
Icon(Icons.Filled.Circle, stringResource(R.string.icon_descr_server_status_connected), tint = MaterialTheme.colors.primaryVariant) Icon(painterResource(R.drawable.ic_circle_filled), stringResource(R.string.icon_descr_server_status_connected), tint = MaterialTheme.colors.primaryVariant)
is Chat.NetworkStatus.Disconnected -> is NetworkStatus.Disconnected ->
Icon(Icons.Filled.Pending, stringResource(R.string.icon_descr_server_status_disconnected), tint = HighOrLowlight) Icon(painterResource(R.drawable.ic_pending_filled), stringResource(R.string.icon_descr_server_status_disconnected), tint = MaterialTheme.colors.secondary)
is Chat.NetworkStatus.Error -> is NetworkStatus.Error ->
Icon(Icons.Filled.Error, stringResource(R.string.icon_descr_server_status_error), tint = HighOrLowlight) Icon(painterResource(R.drawable.ic_error_filled), stringResource(R.string.icon_descr_server_status_error), tint = MaterialTheme.colors.secondary)
else -> Icon(Icons.Outlined.Circle, stringResource(R.string.icon_descr_server_status_pending), tint = HighOrLowlight) else -> Icon(painterResource(R.drawable.ic_circle), stringResource(R.string.icon_descr_server_status_pending), tint = MaterialTheme.colors.secondary)
} }
} }
} }
@@ -380,17 +378,17 @@ fun SwitchAddressButton(onClick: () -> Unit) {
@Composable @Composable
fun VerifyCodeButton(contactVerified: Boolean, onClick: () -> Unit) { fun VerifyCodeButton(contactVerified: Boolean, onClick: () -> Unit) {
SettingsActionItem( SettingsActionItem(
if (contactVerified) Icons.Outlined.VerifiedUser else Icons.Outlined.Shield, if (contactVerified) painterResource(R.drawable.ic_verified_user) else painterResource(R.drawable.ic_shield),
stringResource(if (contactVerified) R.string.view_security_code else R.string.verify_security_code), stringResource(if (contactVerified) R.string.view_security_code else R.string.verify_security_code),
click = onClick, click = onClick,
iconColor = HighOrLowlight, iconColor = MaterialTheme.colors.secondary,
) )
} }
@Composable @Composable
private fun ContactPreferencesButton(onClick: () -> Unit) { private fun ContactPreferencesButton(onClick: () -> Unit) {
SettingsActionItem( SettingsActionItem(
Icons.Outlined.ToggleOn, painterResource(R.drawable.ic_toggle_on),
stringResource(R.string.contact_preferences), stringResource(R.string.contact_preferences),
click = onClick click = onClick
) )
@@ -399,7 +397,7 @@ private fun ContactPreferencesButton(onClick: () -> Unit) {
@Composable @Composable
fun ClearChatButton(onClick: () -> Unit) { fun ClearChatButton(onClick: () -> Unit) {
SettingsActionItem( SettingsActionItem(
Icons.Outlined.Restore, painterResource(R.drawable.ic_settings_backup_restore),
stringResource(R.string.clear_chat_button), stringResource(R.string.clear_chat_button),
click = onClick, click = onClick,
textColor = WarningOrange, textColor = WarningOrange,
@@ -410,7 +408,7 @@ fun ClearChatButton(onClick: () -> Unit) {
@Composable @Composable
private fun DeleteContactButton(onClick: () -> Unit) { private fun DeleteContactButton(onClick: () -> Unit) {
SettingsActionItem( SettingsActionItem(
Icons.Outlined.Delete, painterResource(R.drawable.ic_delete),
stringResource(R.string.button_delete_contact), stringResource(R.string.button_delete_contact),
click = onClick, click = onClick,
textColor = Color.Red, textColor = Color.Red,
@@ -425,13 +423,14 @@ private fun setContactAlias(contactApiId: Long, localAlias: String, chatModel: C
} }
private fun showSwitchContactAddressAlert(m: ChatModel, contactId: Long) { private fun showSwitchContactAddressAlert(m: ChatModel, contactId: Long) {
AlertManager.shared.showAlertMsg( AlertManager.shared.showAlertDialog(
title = generalGetString(R.string.switch_receiving_address_question), title = generalGetString(R.string.switch_receiving_address_question),
text = generalGetString(R.string.switch_receiving_address_desc), text = generalGetString(R.string.switch_receiving_address_desc),
confirmText = generalGetString(R.string.switch_verb), confirmText = generalGetString(R.string.switch_verb),
onConfirm = { onConfirm = {
switchContactAddress(m, contactId) switchContactAddress(m, contactId)
} },
destructive = true,
) )
} }
@@ -446,14 +445,14 @@ fun PreviewChatInfoLayout() {
ChatInfoLayout( ChatInfoLayout(
chat = Chat( chat = Chat(
chatInfo = ChatInfo.Direct.sampleData, chatInfo = ChatInfo.Direct.sampleData,
chatItems = arrayListOf(), chatItems = arrayListOf()
serverInfo = Chat.ServerInfo(Chat.NetworkStatus.Error("agent BROKER TIMEOUT"))
), ),
Contact.sampleData, Contact.sampleData,
localAlias = "", localAlias = "",
connectionCode = "123", connectionCode = "123",
developerTools = false, developerTools = false,
connStats = null, connStats = null,
contactNetworkStatus = NetworkStatus.Connected(),
onLocalAliasChanged = {}, onLocalAliasChanged = {},
customUserProfile = null, customUserProfile = null,
openPreferences = {}, openPreferences = {},
@@ -12,9 +12,6 @@ import androidx.compose.foundation.lazy.*
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.* import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material.icons.outlined.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.mapSaver import androidx.compose.runtime.saveable.mapSaver
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
@@ -23,6 +20,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.* import androidx.compose.ui.platform.*
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.capitalize import androidx.compose.ui.text.capitalize
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
@@ -56,7 +54,13 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: () -> Unit) {
val user = chatModel.currentUser.value val user = chatModel.currentUser.value
val useLinkPreviews = chatModel.controller.appPrefs.privacyLinkPreviews.get() val useLinkPreviews = chatModel.controller.appPrefs.privacyLinkPreviews.get()
val composeState = rememberSaveable(saver = ComposeState.saver()) { val composeState = rememberSaveable(saver = ComposeState.saver()) {
mutableStateOf(ComposeState(useLinkPreviews = useLinkPreviews)) mutableStateOf(
if (chatModel.draftChatId.value == chatId && chatModel.draft.value != null) {
chatModel.draft.value ?: ComposeState(useLinkPreviews = useLinkPreviews)
} else {
ComposeState(useLinkPreviews = useLinkPreviews)
}
)
} }
val attachmentOption = rememberSaveable { mutableStateOf<AttachmentOption?>(null) } val attachmentOption = rememberSaveable { mutableStateOf<AttachmentOption?>(null) }
val attachmentBottomSheetState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden) val attachmentBottomSheetState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden)
@@ -146,9 +150,14 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: () -> Unit) {
} }
} else if (chat.chatInfo is ChatInfo.Group) { } else if (chat.chatInfo is ChatInfo.Group) {
setGroupMembers(chat.chatInfo.groupInfo, chatModel) setGroupMembers(chat.chatInfo.groupInfo, chatModel)
var groupLink = chatModel.controller.apiGetGroupLink(chat.chatInfo.groupInfo.groupId) val link = chatModel.controller.apiGetGroupLink(chat.chatInfo.groupInfo.groupId)
var groupLink = link?.first
var groupLinkMemberRole = link?.second
ModalManager.shared.showModalCloseable(true) { close -> ModalManager.shared.showModalCloseable(true) { close ->
GroupChatInfoView(chatModel, groupLink, { groupLink = it }, close) GroupChatInfoView(chatModel, groupLink, groupLinkMemberRole, {
groupLink = it.first;
groupLinkMemberRole = it.second
}, close)
} }
} }
} }
@@ -187,24 +196,42 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: () -> Unit) {
deleteMessage = { itemId, mode -> deleteMessage = { itemId, mode ->
withApi { withApi {
val cInfo = chat.chatInfo val cInfo = chat.chatInfo
val r = chatModel.controller.apiDeleteChatItem( val toDeleteItem = chatModel.chatItems.firstOrNull { it.id == itemId }
type = cInfo.chatType, val toModerate = toDeleteItem?.memberToModerate(chat.chatInfo)
id = cInfo.apiId, val groupInfo = toModerate?.first
itemId = itemId, val groupMember = toModerate?.second
mode = mode val deletedChatItem: ChatItem?
) val toChatItem: ChatItem?
if (r != null) { if (mode == CIDeleteMode.cidmBroadcast && groupInfo != null && groupMember != null) {
val toChatItem = r.toChatItem val r = chatModel.controller.apiDeleteMemberChatItem(
if (toChatItem == null) { groupId = groupInfo.groupId,
chatModel.removeChatItem(cInfo, r.deletedChatItem.chatItem) groupMemberId = groupMember.groupMemberId,
} else { itemId = itemId
chatModel.upsertChatItem(cInfo, toChatItem.chatItem) )
} deletedChatItem = r?.first
toChatItem = r?.second
} else {
val r = chatModel.controller.apiDeleteChatItem(
type = cInfo.chatType,
id = cInfo.apiId,
itemId = itemId,
mode = mode
)
deletedChatItem = r?.deletedChatItem?.chatItem
toChatItem = r?.toChatItem?.chatItem
}
if (toChatItem == null && deletedChatItem != null) {
chatModel.removeChatItem(cInfo, deletedChatItem)
} else if (toChatItem != null) {
chatModel.upsertChatItem(cInfo, toChatItem)
} }
} }
}, },
receiveFile = { fileId -> receiveFile = { fileId ->
withApi { chatModel.controller.receiveFile(fileId) } withApi { chatModel.controller.receiveFile(user, fileId) }
},
cancelFile = { fileId ->
withApi { chatModel.controller.cancelFile(user, fileId) }
}, },
joinGroup = { groupId -> joinGroup = { groupId ->
withApi { chatModel.controller.apiJoinGroup(groupId) } withApi { chatModel.controller.apiJoinGroup(groupId) }
@@ -284,6 +311,7 @@ fun ChatLayout(
loadPrevMessages: (ChatInfo) -> Unit, loadPrevMessages: (ChatInfo) -> Unit,
deleteMessage: (Long, CIDeleteMode) -> Unit, deleteMessage: (Long, CIDeleteMode) -> Unit,
receiveFile: (Long) -> Unit, receiveFile: (Long) -> Unit,
cancelFile: (Long) -> Unit,
joinGroup: (Long) -> Unit, joinGroup: (Long) -> Unit,
startCall: (CallMediaType) -> Unit, startCall: (CallMediaType) -> Unit,
acceptCall: (Contact) -> Unit, acceptCall: (Contact) -> Unit,
@@ -298,7 +326,7 @@ fun ChatLayout(
Box( Box(
Modifier Modifier
.fillMaxWidth() .fillMaxWidth()
.background(MaterialTheme.colors.background) .themedBackground()
) { ) {
ProvideWindowInsets(windowInsetsAnimationsEnabled = true) { ProvideWindowInsets(windowInsetsAnimationsEnabled = true) {
ModalBottomSheetLayout( ModalBottomSheetLayout(
@@ -328,7 +356,7 @@ fun ChatLayout(
ChatItemsList( ChatItemsList(
chat, unreadCount, composeState, chatItems, searchValue, chat, unreadCount, composeState, chatItems, searchValue,
useLinkPreviews, linkMode, chatModelIncognito, showMemberInfo, loadPrevMessages, deleteMessage, useLinkPreviews, linkMode, chatModelIncognito, showMemberInfo, loadPrevMessages, deleteMessage,
receiveFile, joinGroup, acceptCall, acceptFeature, markRead, setFloatingButton, onComposed, receiveFile, cancelFile, joinGroup, acceptCall, acceptFeature, markRead, setFloatingButton, onComposed,
) )
} }
} }
@@ -348,7 +376,7 @@ fun ChatInfoToolbar(
onSearchValueChanged: (String) -> Unit, onSearchValueChanged: (String) -> Unit,
) { ) {
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
var showMenu by rememberSaveable { mutableStateOf(false) } val showMenu = rememberSaveable { mutableStateOf(false) }
var showSearch by rememberSaveable { mutableStateOf(false) } var showSearch by rememberSaveable { mutableStateOf(false) }
val onBackClicked = { val onBackClicked = {
if (!showSearch) { if (!showSearch) {
@@ -362,34 +390,34 @@ fun ChatInfoToolbar(
val barButtons = arrayListOf<@Composable RowScope.() -> Unit>() val barButtons = arrayListOf<@Composable RowScope.() -> Unit>()
val menuItems = arrayListOf<@Composable () -> Unit>() val menuItems = arrayListOf<@Composable () -> Unit>()
menuItems.add { menuItems.add {
ItemAction(stringResource(android.R.string.search_go).capitalize(Locale.current), Icons.Outlined.Search, onClick = { ItemAction(stringResource(android.R.string.search_go).capitalize(Locale.current), painterResource(R.drawable.ic_search), onClick = {
showMenu = false showMenu.value = false
showSearch = true showSearch = true
}) })
} }
if (chat.chatInfo is ChatInfo.Direct) { if (chat.chatInfo is ChatInfo.Direct && chat.chatInfo.contact.allowsFeature(ChatFeature.Calls)) {
barButtons.add { barButtons.add {
IconButton({ IconButton({
showMenu = false showMenu.value = false
startCall(CallMediaType.Audio) startCall(CallMediaType.Audio)
}) { }) {
Icon(Icons.Outlined.Phone, stringResource(R.string.icon_descr_more_button), tint = MaterialTheme.colors.primary) Icon(painterResource(R.drawable.ic_call_500), stringResource(R.string.icon_descr_more_button), tint = MaterialTheme.colors.primary)
} }
} }
menuItems.add { menuItems.add {
ItemAction(stringResource(R.string.icon_descr_video_call).capitalize(Locale.current), Icons.Outlined.Videocam, onClick = { ItemAction(stringResource(R.string.icon_descr_video_call).capitalize(Locale.current), painterResource(R.drawable.ic_videocam), onClick = {
showMenu = false showMenu.value = false
startCall(CallMediaType.Video) startCall(CallMediaType.Video)
}) })
} }
} else if (chat.chatInfo is ChatInfo.Group && chat.chatInfo.groupInfo.canAddMembers && !chat.chatInfo.incognito) { } else if (chat.chatInfo is ChatInfo.Group && chat.chatInfo.groupInfo.canAddMembers && !chat.chatInfo.incognito) {
barButtons.add { barButtons.add {
IconButton({ IconButton({
showMenu = false showMenu.value = false
addMembers(chat.chatInfo.groupInfo) addMembers(chat.chatInfo.groupInfo)
}) { }) {
Icon(Icons.Outlined.PersonAdd, stringResource(R.string.icon_descr_add_members), tint = MaterialTheme.colors.primary) Icon(painterResource(R.drawable.ic_person_add_500), stringResource(R.string.icon_descr_add_members), tint = MaterialTheme.colors.primary)
} }
} }
} }
@@ -397,9 +425,9 @@ fun ChatInfoToolbar(
menuItems.add { menuItems.add {
ItemAction( ItemAction(
if (ntfsEnabled.value) stringResource(R.string.mute_chat) else stringResource(R.string.unmute_chat), if (ntfsEnabled.value) stringResource(R.string.mute_chat) else stringResource(R.string.unmute_chat),
if (ntfsEnabled.value) Icons.Outlined.NotificationsOff else Icons.Outlined.Notifications, if (ntfsEnabled.value) painterResource(R.drawable.ic_notifications_off) else painterResource(R.drawable.ic_notifications),
onClick = { onClick = {
showMenu = false showMenu.value = false
// Just to make a delay before changing state of ntfsEnabled, otherwise it will redraw menu item with new value before closing the menu // Just to make a delay before changing state of ntfsEnabled, otherwise it will redraw menu item with new value before closing the menu
scope.launch { scope.launch {
delay(200) delay(200)
@@ -410,8 +438,8 @@ fun ChatInfoToolbar(
} }
barButtons.add { barButtons.add {
IconButton({ showMenu = true }) { IconButton({ showMenu.value = true }) {
Icon(Icons.Default.MoreVert, stringResource(R.string.icon_descr_more_button), tint = MaterialTheme.colors.primary) Icon(MoreVertFilled, stringResource(R.string.icon_descr_more_button), tint = MaterialTheme.colors.primary)
} }
} }
@@ -427,18 +455,14 @@ fun ChatInfoToolbar(
Divider(Modifier.padding(top = AppBarHeight)) Divider(Modifier.padding(top = AppBarHeight))
Box(Modifier.fillMaxWidth().wrapContentSize(Alignment.TopEnd).offset(y = AppBarHeight)) { Box(Modifier.fillMaxWidth().wrapContentSize(Alignment.TopEnd).offset(y = AppBarHeight)) {
DropdownMenu( DefaultDropdownMenu(showMenu) {
expanded = showMenu,
onDismissRequest = { showMenu = false },
Modifier.widthIn(min = 220.dp)
) {
menuItems.forEach { it() } menuItems.forEach { it() }
} }
} }
} }
@Composable @Composable
fun ChatInfoToolbarTitle(cInfo: ChatInfo, imageSize: Dp = 40.dp, iconColor: Color = MaterialTheme.colors.secondary) { fun ChatInfoToolbarTitle(cInfo: ChatInfo, imageSize: Dp = 40.dp, iconColor: Color = MaterialTheme.colors.secondaryVariant) {
Row( Row(
horizontalArrangement = Arrangement.Center, horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
@@ -472,7 +496,7 @@ fun ChatInfoToolbarTitle(cInfo: ChatInfo, imageSize: Dp = 40.dp, iconColor: Colo
@Composable @Composable
private fun ContactVerifiedShield() { private fun ContactVerifiedShield() {
Icon(Icons.Outlined.VerifiedUser, null, Modifier.size(18.dp).padding(end = 3.dp, top = 1.dp), tint = HighOrLowlight) Icon(painterResource(R.drawable.ic_verified_user), null, Modifier.size(18.dp).padding(end = 3.dp, top = 1.dp), tint = MaterialTheme.colors.secondary)
} }
data class CIListState(val scrolled: Boolean, val itemCount: Int, val keyboardState: KeyboardState) data class CIListState(val scrolled: Boolean, val itemCount: Int, val keyboardState: KeyboardState)
@@ -501,6 +525,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
loadPrevMessages: (ChatInfo) -> Unit, loadPrevMessages: (ChatInfo) -> Unit,
deleteMessage: (Long, CIDeleteMode) -> Unit, deleteMessage: (Long, CIDeleteMode) -> Unit,
receiveFile: (Long) -> Unit, receiveFile: (Long) -> Unit,
cancelFile: (Long) -> Unit,
joinGroup: (Long) -> Unit, joinGroup: (Long) -> Unit,
acceptCall: (Contact) -> Unit, acceptCall: (Contact) -> Unit,
acceptFeature: (Contact, ChatFeature, Int?) -> Unit, acceptFeature: (Contact, ChatFeature, Int?) -> Unit,
@@ -547,6 +572,11 @@ fun BoxWithConstraintsScope.ChatItemsList(
stopListening = true stopListening = true
} }
} }
DisposableEffectOnGone(
whenGone = {
VideoPlayer.releaseAll()
}
)
LazyColumn(Modifier.align(Alignment.BottomCenter), state = listState, reverseLayout = true) { LazyColumn(Modifier.align(Alignment.BottomCenter), state = listState, reverseLayout = true) {
itemsIndexed(reversedChatItems, key = { _, item -> item.id}) { i, cItem -> itemsIndexed(reversedChatItems, key = { _, item -> item.id}) { i, cItem ->
CompositionLocalProvider( CompositionLocalProvider(
@@ -566,10 +596,12 @@ fun BoxWithConstraintsScope.ChatItemsList(
if (dismissState.isAnimationRunning && (swipedToStart || swipedToEnd)) { if (dismissState.isAnimationRunning && (swipedToStart || swipedToEnd)) {
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
scope.launch { scope.launch {
if (composeState.value.editing) { if (cItem.content is CIContent.SndMsgContent || cItem.content is CIContent.RcvMsgContent) {
composeState.value = ComposeState(contextItem = ComposeContextItem.QuotedItem(cItem), useLinkPreviews = useLinkPreviews) if (composeState.value.editing) {
} else if (cItem.id != ChatItem.TEMP_LIVE_CHAT_ITEM_ID) { composeState.value = ComposeState(contextItem = ComposeContextItem.QuotedItem(cItem), useLinkPreviews = useLinkPreviews)
composeState.value = composeState.value.copy(contextItem = ComposeContextItem.QuotedItem(cItem)) } else if (cItem.id != ChatItem.TEMP_LIVE_CHAT_ITEM_ID) {
composeState.value = composeState.value.copy(contextItem = ComposeContextItem.QuotedItem(cItem))
}
} }
} }
} }
@@ -609,11 +641,11 @@ fun BoxWithConstraintsScope.ChatItemsList(
} else { } else {
Spacer(Modifier.size(42.dp)) Spacer(Modifier.size(42.dp))
} }
ChatItemView(chat.chatInfo, cItem, composeState, provider, showMember = showMember, useLinkPreviews = useLinkPreviews, linkMode = linkMode, deleteMessage = deleteMessage, receiveFile = receiveFile, joinGroup = {}, acceptCall = acceptCall, acceptFeature = acceptFeature, scrollToItem = scrollToItem) ChatItemView(chat.chatInfo, cItem, composeState, provider, showMember = showMember, useLinkPreviews = useLinkPreviews, linkMode = linkMode, deleteMessage = deleteMessage, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = {}, acceptCall = acceptCall, acceptFeature = acceptFeature, scrollToItem = scrollToItem)
} }
} else { } else {
Box(Modifier.padding(start = 104.dp, end = 12.dp).then(swipeableModifier)) { Box(Modifier.padding(start = 104.dp, end = 12.dp).then(swipeableModifier)) {
ChatItemView(chat.chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, deleteMessage = deleteMessage, receiveFile = receiveFile, joinGroup = {}, acceptCall = acceptCall, acceptFeature = acceptFeature, scrollToItem = scrollToItem) ChatItemView(chat.chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, deleteMessage = deleteMessage, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = {}, acceptCall = acceptCall, acceptFeature = acceptFeature, scrollToItem = scrollToItem)
} }
} }
} else { // direct message } else { // direct message
@@ -624,7 +656,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
end = if (sent) 12.dp else 76.dp, end = if (sent) 12.dp else 76.dp,
).then(swipeableModifier) ).then(swipeableModifier)
) { ) {
ChatItemView(chat.chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, deleteMessage = deleteMessage, receiveFile = receiveFile, joinGroup = joinGroup, acceptCall = acceptCall, acceptFeature = acceptFeature, scrollToItem = scrollToItem) ChatItemView(chat.chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, deleteMessage = deleteMessage, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = joinGroup, acceptCall = acceptCall, acceptFeature = acceptFeature, scrollToItem = scrollToItem)
} }
} }
@@ -665,10 +697,18 @@ private fun ScrollToBottom(chatId: ChatId, listState: LazyListState, chatItems:
.distinctUntilChanged() .distinctUntilChanged()
.filter { listState.layoutInfo.visibleItemsInfo.firstOrNull()?.key != it } .filter { listState.layoutInfo.visibleItemsInfo.firstOrNull()?.key != it }
.collect { .collect {
if (listState.firstVisibleItemIndex == 0) { try {
listState.animateScrollToItem(0) if (listState.firstVisibleItemIndex == 0) {
} else { listState.animateScrollToItem(0)
listState.animateScrollBy(scrollDistance) } else {
listState.animateScrollBy(scrollDistance)
}
} catch (e: CancellationException) {
/**
* When you tap and hold a finger on a lazy column with chatItems, and then you receive a message,
* this coroutine will be canceled with the message "Current mutation had a higher priority" because of animatedScroll.
* Which breaks auto-scrolling to bottom. So just ignoring the exception
* */
} }
} }
} }
@@ -742,36 +782,27 @@ fun BoxWithConstraintsScope.FloatingButtons(
} }
val showButtonWithCounter = topUnreadCount > 0 val showButtonWithCounter = topUnreadCount > 0
val height = with(LocalDensity.current) { maxHeight.toPx() } val height = with(LocalDensity.current) { maxHeight.toPx() }
var showDropDown by remember { mutableStateOf(false) } val showDropDown = remember { mutableStateOf(false) }
TopEndFloatingButton( TopEndFloatingButton(
Modifier.padding(end = 16.dp, top = 24.dp).align(Alignment.TopEnd), Modifier.padding(end = DEFAULT_PADDING, top = 24.dp).align(Alignment.TopEnd),
topUnreadCount, topUnreadCount,
showButtonWithCounter, showButtonWithCounter,
onClick = { scope.launch { listState.animateScrollBy(height) } }, onClick = { scope.launch { listState.animateScrollBy(height) } },
onLongClick = { showDropDown = true } onLongClick = { showDropDown.value = true }
) )
DropdownMenu( DefaultDropdownMenu(showDropDown, offset = DpOffset(maxWidth - DEFAULT_PADDING, 24.dp + fabSize)) {
expanded = showDropDown, ItemAction(
onDismissRequest = { showDropDown = false }, generalGetString(R.string.mark_read),
Modifier.width(220.dp), painterResource(R.drawable.ic_check),
offset = DpOffset(maxWidth - 16.dp, 24.dp + fabSize)
) {
DropdownMenuItem(
onClick = { onClick = {
markRead( markRead(
CC.ItemRange(minUnreadItemId, chatItems[chatItems.size - listState.layoutInfo.visibleItemsInfo.lastIndex - 1].id - 1), CC.ItemRange(minUnreadItemId, chatItems[chatItems.size - listState.layoutInfo.visibleItemsInfo.lastIndex - 1].id - 1),
bottomUnreadCount bottomUnreadCount
) )
showDropDown = false showDropDown.value = false
} })
) {
Text(
generalGetString(R.string.mark_read),
maxLines = 1,
)
}
} }
} }
@@ -868,7 +899,7 @@ private fun bottomEndFloatingButton(
modifier = Modifier.size(48.dp) modifier = Modifier.size(48.dp)
) { ) {
Icon( Icon(
imageVector = Icons.Default.KeyboardArrowDown, painter = painterResource(R.drawable.ic_keyboard_arrow_down),
contentDescription = null, contentDescription = null,
tint = MaterialTheme.colors.primary tint = MaterialTheme.colors.primary
) )
@@ -896,21 +927,26 @@ private fun markUnreadChatAsRead(activeChat: MutableState<Chat?>, chatModel: Cha
} }
} }
sealed class ProviderMedia {
data class Image(val uri: Uri, val image: Bitmap): ProviderMedia()
data class Video(val uri: Uri, val preview: String): ProviderMedia()
}
private fun providerForGallery( private fun providerForGallery(
listStateIndex: Int, listStateIndex: Int,
chatItems: List<ChatItem>, chatItems: List<ChatItem>,
cItemId: Long, cItemId: Long,
scrollTo: (Int) -> Unit scrollTo: (Int) -> Unit
): ImageGalleryProvider { ): ImageGalleryProvider {
fun canShowImage(item: ChatItem): Boolean = fun canShowMedia(item: ChatItem): Boolean =
item.content.msgContent is MsgContent.MCImage && item.file?.loaded == true && getLoadedFilePath(SimplexApp.context, item.file) != null (item.content.msgContent is MsgContent.MCImage || item.content.msgContent is MsgContent.MCVideo) && (item.file?.loaded == true && getLoadedFilePath(SimplexApp.context, item.file) != null)
fun item(skipInternalIndex: Int, initialChatId: Long): Pair<Int, ChatItem>? { fun item(skipInternalIndex: Int, initialChatId: Long): Pair<Int, ChatItem>? {
var processedInternalIndex = -skipInternalIndex.sign var processedInternalIndex = -skipInternalIndex.sign
val indexOfFirst = chatItems.indexOfFirst { it.id == initialChatId } val indexOfFirst = chatItems.indexOfFirst { it.id == initialChatId }
for (chatItemsIndex in if (skipInternalIndex >= 0) indexOfFirst downTo 0 else indexOfFirst..chatItems.lastIndex) { for (chatItemsIndex in if (skipInternalIndex >= 0) indexOfFirst downTo 0 else indexOfFirst..chatItems.lastIndex) {
val item = chatItems[chatItemsIndex] val item = chatItems[chatItemsIndex]
if (canShowImage(item)) { if (canShowMedia(item)) {
processedInternalIndex += skipInternalIndex.sign processedInternalIndex += skipInternalIndex.sign
} }
if (processedInternalIndex == skipInternalIndex) { if (processedInternalIndex == skipInternalIndex) {
@@ -924,16 +960,28 @@ private fun providerForGallery(
var initialChatId = cItemId var initialChatId = cItemId
return object: ImageGalleryProvider { return object: ImageGalleryProvider {
override val initialIndex: Int = initialIndex override val initialIndex: Int = initialIndex
override val totalImagesSize = mutableStateOf(Int.MAX_VALUE) override val totalMediaSize = mutableStateOf(Int.MAX_VALUE)
override fun getImage(index: Int): Pair<Bitmap, Uri>? { override fun getMedia(index: Int): ProviderMedia? {
val internalIndex = initialIndex - index val internalIndex = initialIndex - index
val file = item(internalIndex, initialChatId)?.second?.file val item = item(internalIndex, initialChatId)?.second ?: return null
val imageBitmap: Bitmap? = getLoadedImage(SimplexApp.context, file) return when (item.content.msgContent) {
val filePath = getLoadedFilePath(SimplexApp.context, file) is MsgContent.MCImage -> {
return if (imageBitmap != null && filePath != null) { val imageBitmap: Bitmap? = getLoadedImage(SimplexApp.context, item.file)
val uri = FileProvider.getUriForFile(SimplexApp.context, "${BuildConfig.APPLICATION_ID}.provider", File(filePath)) val filePath = getLoadedFilePath(SimplexApp.context, item.file)
imageBitmap to uri if (imageBitmap != null && filePath != null) {
} else null val uri = FileProvider.getUriForFile(SimplexApp.context, "${BuildConfig.APPLICATION_ID}.provider", File(filePath))
ProviderMedia.Image(uri, imageBitmap)
} else null
}
is MsgContent.MCVideo -> {
val filePath = getLoadedFilePath(SimplexApp.context, item.file)
if (filePath != null) {
val uri = FileProvider.getUriForFile(SimplexApp.context, "${BuildConfig.APPLICATION_ID}.provider", File(filePath))
ProviderMedia.Video(uri, (item.content.msgContent as MsgContent.MCVideo).image)
} else null
}
else -> null
}
} }
override fun currentPageChanged(index: Int) { override fun currentPageChanged(index: Int) {
@@ -945,7 +993,7 @@ private fun providerForGallery(
override fun scrollToStart() { override fun scrollToStart() {
initialIndex = 0 initialIndex = 0
initialChatId = chatItems.first { canShowImage(it) }.id initialChatId = chatItems.first { canShowMedia(it) }.id
} }
override fun onDismiss(index: Int) { override fun onDismiss(index: Int) {
@@ -1023,6 +1071,7 @@ fun PreviewChatLayout() {
loadPrevMessages = { _ -> }, loadPrevMessages = { _ -> },
deleteMessage = { _, _ -> }, deleteMessage = { _, _ -> },
receiveFile = {}, receiveFile = {},
cancelFile = {},
joinGroup = {}, joinGroup = {},
startCall = {}, startCall = {},
acceptCall = { _ -> }, acceptCall = { _ -> },
@@ -1082,6 +1131,7 @@ fun PreviewGroupChatLayout() {
loadPrevMessages = { _ -> }, loadPrevMessages = { _ -> },
deleteMessage = { _, _ -> }, deleteMessage = { _, _ -> },
receiveFile = {}, receiveFile = {},
cancelFile = {},
joinGroup = {}, joinGroup = {},
startCall = {}, startCall = {},
acceptCall = { _ -> }, acceptCall = { _ -> },
@@ -1,31 +1,30 @@
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.material.* import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.InsertDriveFile
import androidx.compose.material.icons.outlined.Close
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import chat.simplex.app.R import chat.simplex.app.R
import chat.simplex.app.ui.theme.* import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.chat.item.SentColorLight
@Composable @Composable
fun ComposeFileView(fileName: String, cancelFile: () -> Unit, cancelEnabled: Boolean) { fun ComposeFileView(fileName: String, cancelFile: () -> Unit, cancelEnabled: Boolean) {
val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage
Row( Row(
Modifier Modifier
.height(60.dp) .height(60.dp)
.fillMaxWidth() .fillMaxWidth()
.padding(top = 8.dp) .padding(top = 8.dp)
.background(SentColorLight), .background(sentColor),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Icon( Icon(
Icons.Filled.InsertDriveFile, painterResource(R.drawable.ic_draft_filled),
stringResource(R.string.icon_descr_file), stringResource(R.string.icon_descr_file),
Modifier Modifier
.padding(start = 4.dp, end = 2.dp) .padding(start = 4.dp, end = 2.dp)
@@ -37,7 +36,7 @@ fun ComposeFileView(fileName: String, cancelFile: () -> Unit, cancelEnabled: Boo
if (cancelEnabled) { if (cancelEnabled) {
IconButton(onClick = cancelFile, modifier = Modifier.padding(0.dp)) { IconButton(onClick = cancelFile, modifier = Modifier.padding(0.dp)) {
Icon( Icon(
Icons.Outlined.Close, painterResource(R.drawable.ic_close),
contentDescription = stringResource(R.string.icon_descr_cancel_file_preview), contentDescription = stringResource(R.string.icon_descr_cancel_file_preview),
tint = MaterialTheme.colors.primary, tint = MaterialTheme.colors.primary,
modifier = Modifier.padding(10.dp) modifier = Modifier.padding(10.dp)
@@ -4,26 +4,28 @@ import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material.* import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Close
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import chat.simplex.app.R import chat.simplex.app.R
import chat.simplex.app.ui.theme.DEFAULT_PADDING_HALF import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.chat.item.SentColorLight import chat.simplex.app.views.helpers.UploadContent
import chat.simplex.app.views.helpers.base64ToBitmap import chat.simplex.app.views.helpers.base64ToBitmap
@Composable @Composable
fun ComposeImageView(images: List<String>, cancelImages: () -> Unit, cancelEnabled: Boolean) { fun ComposeImageView(media: ComposePreview.MediaPreview, cancelImages: () -> Unit, cancelEnabled: Boolean) {
val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage
Row( Row(
Modifier Modifier
.padding(top = 8.dp) .padding(top = 8.dp)
.background(SentColorLight), .background(sentColor),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
LazyRow( LazyRow(
@@ -31,19 +33,38 @@ fun ComposeImageView(images: List<String>, cancelImages: () -> Unit, cancelEnabl
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(DEFAULT_PADDING_HALF), horizontalArrangement = Arrangement.spacedBy(DEFAULT_PADDING_HALF),
) { ) {
items(images.size) { index -> itemsIndexed(media.images) { index, item ->
val imageBitmap = base64ToBitmap(images[index]).asImageBitmap() val content = media.content[index]
Image( if (content is UploadContent.Video) {
imageBitmap, Box(contentAlignment = Alignment.Center) {
"preview image", val imageBitmap = base64ToBitmap(item).asImageBitmap()
modifier = Modifier.widthIn(max = 80.dp).height(60.dp) Image(
) imageBitmap,
"preview video",
modifier = Modifier.widthIn(max = 80.dp).height(60.dp)
)
Icon(
painterResource(R.drawable.ic_videocam_filled),
"preview video",
Modifier
.size(20.dp),
tint = Color.White
)
}
} else {
val imageBitmap = base64ToBitmap(item).asImageBitmap()
Image(
imageBitmap,
"preview image",
modifier = Modifier.widthIn(max = 80.dp).height(60.dp)
)
}
} }
} }
if (cancelEnabled) { if (cancelEnabled) {
IconButton(onClick = cancelImages) { IconButton(onClick = cancelImages) {
Icon( Icon(
Icons.Outlined.Close, painterResource(R.drawable.ic_close),
contentDescription = stringResource(R.string.icon_descr_cancel_image_preview), contentDescription = stringResource(R.string.icon_descr_cancel_image_preview),
tint = MaterialTheme.colors.primary, tint = MaterialTheme.colors.primary,
) )
@@ -1,27 +1,24 @@
@file:UseSerializers(UriSerializer::class)
package chat.simplex.app.views.chat package chat.simplex.app.views.chat
import ComposeVoiceView
import ComposeFileView import ComposeFileView
import ComposeVoiceView
import android.Manifest import android.Manifest
import android.app.Activity import android.app.Activity
import android.content.* import android.content.*
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.graphics.Bitmap import android.graphics.*
import android.graphics.ImageDecoder
import android.graphics.ImageDecoder.DecodeException
import android.graphics.drawable.AnimatedImageDrawable import android.graphics.drawable.AnimatedImageDrawable
import android.net.Uri import android.net.Uri
import android.os.Build
import android.provider.MediaStore import android.provider.MediaStore
import android.util.Log import android.webkit.MimeTypeMap
import android.widget.Toast import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContract import androidx.activity.result.contract.ActivityResultContract
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.* import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material.icons.outlined.Reply
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.Saver import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
@@ -29,31 +26,28 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.net.toFile
import androidx.core.net.toUri
import chat.simplex.app.* import chat.simplex.app.*
import chat.simplex.app.R import chat.simplex.app.R
import chat.simplex.app.model.* import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.HighOrLowlight
import chat.simplex.app.views.chat.item.* import chat.simplex.app.views.chat.item.*
import chat.simplex.app.views.helpers.* import chat.simplex.app.views.helpers.*
import kotlinx.coroutines.delay import kotlinx.coroutines.*
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.runBlocking import kotlinx.serialization.*
import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import java.io.File import java.io.File
import java.nio.file.Files
@Serializable @Serializable
sealed class ComposePreview { sealed class ComposePreview {
@Serializable object NoPreview: ComposePreview() @Serializable object NoPreview: ComposePreview()
@Serializable class CLinkPreview(val linkPreview: LinkPreview?): ComposePreview() @Serializable class CLinkPreview(val linkPreview: LinkPreview?): ComposePreview()
@Serializable class ImagePreview(val images: List<String>): ComposePreview() @Serializable class MediaPreview(val images: List<String>, val content: List<UploadContent>): ComposePreview()
@Serializable class VoicePreview(val voice: String, val durationMs: Int, val finished: Boolean): ComposePreview() @Serializable data class VoicePreview(val voice: String, val durationMs: Int, val finished: Boolean): ComposePreview()
@Serializable class FilePreview(val fileName: String): ComposePreview() @Serializable class FilePreview(val fileName: String, val uri: Uri): ComposePreview()
} }
@Serializable @Serializable
@@ -97,7 +91,7 @@ data class ComposeState(
val sendEnabled: () -> Boolean val sendEnabled: () -> Boolean
get() = { get() = {
val hasContent = when (preview) { val hasContent = when (preview) {
is ComposePreview.ImagePreview -> true is ComposePreview.MediaPreview -> true
is ComposePreview.VoicePreview -> true is ComposePreview.VoicePreview -> true
is ComposePreview.FilePreview -> true is ComposePreview.FilePreview -> true
else -> message.isNotEmpty() || liveMessage != null else -> message.isNotEmpty() || liveMessage != null
@@ -110,7 +104,7 @@ data class ComposeState(
val linkPreviewAllowed: Boolean val linkPreviewAllowed: Boolean
get() = get() =
when (preview) { when (preview) {
is ComposePreview.ImagePreview -> false is ComposePreview.MediaPreview -> false
is ComposePreview.VoicePreview -> false is ComposePreview.VoicePreview -> false
is ComposePreview.FilePreview -> false is ComposePreview.FilePreview -> false
else -> useLinkPreviews else -> useLinkPreviews
@@ -132,6 +126,9 @@ data class ComposeState(
} }
} }
val empty: Boolean
get() = message.isEmpty() && preview is ComposePreview.NoPreview
companion object { companion object {
fun saver(): Saver<MutableState<ComposeState>, *> = Saver( fun saver(): Saver<MutableState<ComposeState>, *> = Saver(
save = { json.encodeToString(serializer(), it.value) }, save = { json.encodeToString(serializer(), it.value) },
@@ -152,15 +149,15 @@ sealed class RecordingState {
} }
fun chatItemPreview(chatItem: ChatItem): ComposePreview { fun chatItemPreview(chatItem: ChatItem): ComposePreview {
val fileName = chatItem.file?.fileName ?: ""
return when (val mc = chatItem.content.msgContent) { return when (val mc = chatItem.content.msgContent) {
is MsgContent.MCText -> ComposePreview.NoPreview is MsgContent.MCText -> ComposePreview.NoPreview
is MsgContent.MCLink -> ComposePreview.CLinkPreview(linkPreview = mc.preview) is MsgContent.MCLink -> ComposePreview.CLinkPreview(linkPreview = mc.preview)
is MsgContent.MCImage -> ComposePreview.ImagePreview(images = listOf(mc.image)) // TODO: include correct type
is MsgContent.MCVoice -> ComposePreview.VoicePreview(voice = chatItem.file?.fileName ?: "", mc.duration / 1000, true) is MsgContent.MCImage -> ComposePreview.MediaPreview(images = listOf(mc.image), listOf(UploadContent.SimpleImage(getAppFileUri(fileName))))
is MsgContent.MCFile -> { is MsgContent.MCVideo -> ComposePreview.MediaPreview(images = listOf(mc.image), listOf(UploadContent.SimpleImage(getAppFileUri(fileName))))
val fileName = chatItem.file?.fileName ?: "" is MsgContent.MCVoice -> ComposePreview.VoicePreview(voice = fileName, mc.duration / 1000, true)
ComposePreview.FilePreview(fileName) is MsgContent.MCFile -> ComposePreview.FilePreview(fileName, getAppFileUri(fileName))
}
is MsgContent.MCUnknown, null -> ComposePreview.NoPreview is MsgContent.MCUnknown, null -> ComposePreview.NoPreview
} }
} }
@@ -179,23 +176,16 @@ fun ComposeView(
val pendingLinkUrl = rememberSaveable { mutableStateOf<String?>(null) } val pendingLinkUrl = rememberSaveable { mutableStateOf<String?>(null) }
val cancelledLinks = rememberSaveable { mutableSetOf<String>() } val cancelledLinks = rememberSaveable { mutableSetOf<String>() }
val useLinkPreviews = chatModel.controller.appPrefs.privacyLinkPreviews.get() val useLinkPreviews = chatModel.controller.appPrefs.privacyLinkPreviews.get()
val maxFileSize = getMaxFileSize(FileProtocol.XFTP)
val smallFont = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.onBackground) val smallFont = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.onBackground)
val textStyle = remember { mutableStateOf(smallFont) } val textStyle = remember { mutableStateOf(smallFont) }
// attachments
val chosenContent = rememberSaveable { mutableStateOf<List<UploadContent>>(emptyList()) }
val audioSaver = Saver<MutableState<Pair<Uri, Int>?>, Pair<String, Int>>(
save = { it.value.let { if (it == null) null else it.first.toString() to it.second } },
restore = { mutableStateOf(Uri.parse(it.first) to it.second) }
)
val chosenAudio = rememberSaveable(saver = audioSaver) { mutableStateOf(null) }
val chosenFile = rememberSaveable { mutableStateOf<Uri?>(null) }
val cameraLauncher = rememberCameraLauncher { uri: Uri? -> val cameraLauncher = rememberCameraLauncher { uri: Uri? ->
if (uri != null) { if (uri != null) {
val source = ImageDecoder.createSource(SimplexApp.context.contentResolver, uri) val bitmap: Bitmap? = getBitmapFromUri(uri)
val bitmap = ImageDecoder.decodeBitmap(source) if (bitmap != null) {
val imagePreview = resizeImageToStrSize(bitmap, maxDataSize = 14000) val imagePreview = resizeImageToStrSize(bitmap, maxDataSize = 14000)
chosenContent.value = listOf(UploadContent.SimpleImage(uri)) composeState.value = composeState.value.copy(preview = ComposePreview.MediaPreview(listOf(imagePreview), listOf(UploadContent.SimpleImage(uri))))
composeState.value = composeState.value.copy(preview = ComposePreview.ImagePreview(listOf(imagePreview))) }
} }
} }
val cameraPermissionLauncher = rememberPermissionLauncher { isGranted: Boolean -> val cameraPermissionLauncher = rememberPermissionLauncher { isGranted: Boolean ->
@@ -205,72 +195,79 @@ fun ComposeView(
Toast.makeText(context, generalGetString(R.string.toast_permission_denied), Toast.LENGTH_SHORT).show() Toast.makeText(context, generalGetString(R.string.toast_permission_denied), Toast.LENGTH_SHORT).show()
} }
} }
val processPickedImage = { uris: List<Uri>, text: String? -> val processPickedMedia = { uris: List<Uri>, text: String? ->
val content = ArrayList<UploadContent>() val content = ArrayList<UploadContent>()
val imagesPreview = ArrayList<String>() val imagesPreview = ArrayList<String>()
uris.forEach { uri -> uris.forEach { uri ->
val source = ImageDecoder.createSource(context.contentResolver, uri) var bitmap: Bitmap? = null
val drawable = try { val isImage = MimeTypeMap.getSingleton().getMimeTypeFromExtension(getFileName(SimplexApp.context, uri)?.split(".")?.last())?.contains("image/") == true
ImageDecoder.decodeDrawable(source) when {
} catch (e: DecodeException) { isImage -> {
AlertManager.shared.showAlertMsg( // Image
title = generalGetString(R.string.image_decoding_exception_title), val drawable = getDrawableFromUri(uri)
text = generalGetString(R.string.image_decoding_exception_desc) bitmap = if (drawable != null) getBitmapFromUri(uri) else null
) val isAnimNewApi = Build.VERSION.SDK_INT >= 28 && drawable is AnimatedImageDrawable
Log.e(TAG, "Error while decoding drawable: ${e.stackTraceToString()}") val isAnimOldApi = Build.VERSION.SDK_INT < 28 &&
null (getFileName(SimplexApp.context, uri)?.endsWith(".gif") == true || getFileName(SimplexApp.context, uri)?.endsWith(".webp") == true)
} if (isAnimNewApi || isAnimOldApi) {
var bitmap: Bitmap? = if (drawable != null) ImageDecoder.decodeBitmap(source) else null // It's a gif or webp
if (drawable is AnimatedImageDrawable) { val fileSize = getFileSize(context, uri)
// It's a gif or webp if (fileSize != null && fileSize <= maxFileSize) {
val fileSize = getFileSize(context, uri) content.add(UploadContent.AnimatedImage(uri))
if (fileSize != null && fileSize <= MAX_FILE_SIZE) { } else {
content.add(UploadContent.AnimatedImage(uri)) bitmap = null
} else { AlertManager.shared.showAlertMsg(
bitmap = null generalGetString(R.string.large_file),
AlertManager.shared.showAlertMsg( String.format(generalGetString(R.string.maximum_supported_file_size), formatBytes(maxFileSize))
generalGetString(R.string.large_file), )
String.format(generalGetString(R.string.maximum_supported_file_size), formatBytes(MAX_FILE_SIZE)) }
) } else {
content.add(UploadContent.SimpleImage(uri))
}
}
else -> {
// Video
val res = getBitmapFromVideo(uri)
bitmap = res.preview
val durationMs = res.duration
content.add(UploadContent.Video(uri, durationMs?.div(1000)?.toInt() ?: 0))
} }
} else {
content.add(UploadContent.SimpleImage(uri))
} }
if (bitmap != null) { if (bitmap != null) {
imagesPreview.add(resizeImageToStrSize(bitmap, maxDataSize = 14000)) imagesPreview.add(resizeImageToStrSize(bitmap, maxDataSize = 14000))
} }
} }
if (imagesPreview.isNotEmpty()) { if (imagesPreview.isNotEmpty()) {
chosenContent.value = content composeState.value = composeState.value.copy(message = text ?: composeState.value.message, preview = ComposePreview.MediaPreview(imagesPreview, content))
composeState.value = composeState.value.copy(message = text ?: composeState.value.message, preview = ComposePreview.ImagePreview(imagesPreview))
} }
} }
val processPickedFile = { uri: Uri?, text: String? -> val processPickedFile = { uri: Uri?, text: String? ->
if (uri != null) { if (uri != null) {
val fileSize = getFileSize(context, uri) val fileSize = getFileSize(context, uri)
if (fileSize != null && fileSize <= MAX_FILE_SIZE) { if (fileSize != null && fileSize <= maxFileSize) {
val fileName = getFileName(SimplexApp.context, uri) val fileName = getFileName(SimplexApp.context, uri)
if (fileName != null) { if (fileName != null) {
chosenFile.value = uri composeState.value = composeState.value.copy(message = text ?: composeState.value.message, preview = ComposePreview.FilePreview(fileName, uri))
composeState.value = composeState.value.copy(message = text ?: composeState.value.message, preview = ComposePreview.FilePreview(fileName))
} }
} else { } else {
AlertManager.shared.showAlertMsg( AlertManager.shared.showAlertMsg(
generalGetString(R.string.large_file), generalGetString(R.string.large_file),
String.format(generalGetString(R.string.maximum_supported_file_size), formatBytes(MAX_FILE_SIZE)) String.format(generalGetString(R.string.maximum_supported_file_size), formatBytes(maxFileSize))
) )
} }
} }
} }
val galleryLauncher = rememberLauncherForActivityResult(contract = PickMultipleFromGallery()) { processPickedImage(it, null) } val galleryImageLauncher = rememberLauncherForActivityResult(contract = PickMultipleImagesFromGallery()) { processPickedMedia(it, null) }
val galleryLauncherFallback = rememberGetMultipleContentsLauncher { processPickedImage(it, null) } val galleryImageLauncherFallback = rememberGetMultipleContentsLauncher { processPickedMedia(it, null) }
val galleryVideoLauncher = rememberLauncherForActivityResult(contract = PickMultipleVideosFromGallery()) { processPickedMedia(it, null) }
val galleryVideoLauncherFallback = rememberGetMultipleContentsLauncher { processPickedMedia(it, null) }
val filesLauncher = rememberGetContentLauncher { processPickedFile(it, null) } val filesLauncher = rememberGetContentLauncher { processPickedFile(it, null) }
val recState: MutableState<RecordingState> = remember { mutableStateOf(RecordingState.NotStarted) } val recState: MutableState<RecordingState> = remember { mutableStateOf(RecordingState.NotStarted) }
LaunchedEffect(attachmentOption.value) { LaunchedEffect(attachmentOption.value) {
when (attachmentOption.value) { when (attachmentOption.value) {
AttachmentOption.TakePhoto -> { AttachmentOption.CameraPhoto -> {
when (PackageManager.PERMISSION_GRANTED) { when (PackageManager.PERMISSION_GRANTED) {
ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) -> { ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) -> {
cameraLauncher.launchWithFallback() cameraLauncher.launchWithFallback()
@@ -281,15 +278,23 @@ fun ComposeView(
} }
attachmentOption.value = null attachmentOption.value = null
} }
AttachmentOption.PickImage -> { AttachmentOption.GalleryImage -> {
try { try {
galleryLauncher.launch(0) galleryImageLauncher.launch(0)
} catch (e: ActivityNotFoundException) { } catch (e: ActivityNotFoundException) {
galleryLauncherFallback.launch("image/*") galleryImageLauncherFallback.launch("image/*")
} }
attachmentOption.value = null attachmentOption.value = null
} }
AttachmentOption.PickFile -> { AttachmentOption.GalleryVideo -> {
try {
galleryVideoLauncher.launch(0)
} catch (e: ActivityNotFoundException) {
galleryVideoLauncherFallback.launch("video/*")
}
attachmentOption.value = null
}
AttachmentOption.File -> {
filesLauncher.launch("*/*") filesLauncher.launch("*/*")
attachmentOption.value = null attachmentOption.value = null
} }
@@ -353,12 +358,14 @@ fun ComposeView(
} }
recState.value = RecordingState.NotStarted recState.value = RecordingState.NotStarted
textStyle.value = smallFont textStyle.value = smallFont
chosenContent.value = emptyList()
chosenAudio.value = null
chosenFile.value = null
chatModel.removeLiveDummy() chatModel.removeLiveDummy()
} }
fun deleteUnusedFiles() {
chatModel.filesToDelete.forEach { it.delete() }
chatModel.filesToDelete.clear()
}
suspend fun send(cInfo: ChatInfo, mc: MsgContent, quoted: Long?, file: String? = null, live: Boolean = false): ChatItem? { suspend fun send(cInfo: ChatInfo, mc: MsgContent, quoted: Long?, file: String? = null, live: Boolean = false): ChatItem? {
val aChatItem = chatModel.controller.apiSendMessage( val aChatItem = chatModel.controller.apiSendMessage(
type = cInfo.chatType, type = cInfo.chatType,
@@ -404,6 +411,7 @@ fun ComposeView(
is MsgContent.MCText -> checkLinkPreview() is MsgContent.MCText -> checkLinkPreview()
is MsgContent.MCLink -> checkLinkPreview() is MsgContent.MCLink -> checkLinkPreview()
is MsgContent.MCImage -> MsgContent.MCImage(msgText, image = msgContent.image) is MsgContent.MCImage -> MsgContent.MCImage(msgText, image = msgContent.image)
is MsgContent.MCVideo -> MsgContent.MCVideo(msgText, image = msgContent.image, duration = msgContent.duration)
is MsgContent.MCVoice -> MsgContent.MCVoice(msgText, duration = msgContent.duration) is MsgContent.MCVoice -> MsgContent.MCVoice(msgText, duration = msgContent.duration)
is MsgContent.MCFile -> MsgContent.MCFile(msgText) is MsgContent.MCFile -> MsgContent.MCFile(msgText)
is MsgContent.MCUnknown -> MsgContent.MCUnknown(type = msgContent.type, text = msgText, json = msgContent.json) is MsgContent.MCUnknown -> MsgContent.MCUnknown(type = msgContent.type, text = msgText, json = msgContent.json)
@@ -443,36 +451,39 @@ fun ComposeView(
when (val preview = cs.preview) { when (val preview = cs.preview) {
ComposePreview.NoPreview -> msgs.add(MsgContent.MCText(msgText)) ComposePreview.NoPreview -> msgs.add(MsgContent.MCText(msgText))
is ComposePreview.CLinkPreview -> msgs.add(checkLinkPreview()) is ComposePreview.CLinkPreview -> msgs.add(checkLinkPreview())
is ComposePreview.ImagePreview -> { is ComposePreview.MediaPreview -> {
chosenContent.value.forEachIndexed { index, it -> preview.content.forEachIndexed { index, it ->
val file = when (it) { val file = when (it) {
is UploadContent.SimpleImage -> saveImage(context, it.uri) is UploadContent.SimpleImage -> saveImage(context, it.uri)
is UploadContent.AnimatedImage -> saveAnimImage(context, it.uri) is UploadContent.AnimatedImage -> saveAnimImage(context, it.uri)
is UploadContent.Video -> saveFileFromUri(context, it.uri)
} }
if (file != null) { if (file != null) {
files.add(file) files.add(file)
msgs.add(MsgContent.MCImage(if (chosenContent.value.lastIndex == index) msgText else "", preview.images[index])) if (it is UploadContent.Video) {
msgs.add(MsgContent.MCVideo(if (preview.content.lastIndex == index) msgText else "", preview.images[index], it.duration))
} else {
msgs.add(MsgContent.MCImage(if (preview.content.lastIndex == index) msgText else "", preview.images[index]))
}
} }
} }
} }
is ComposePreview.VoicePreview -> { is ComposePreview.VoicePreview -> {
val chosenAudioVal = chosenAudio.value val tmpFile = File(preview.voice)
if (chosenAudioVal != null) { AudioPlayer.stop(tmpFile.absolutePath)
val file = chosenAudioVal.first.toFile().name val actualFile = File(getAppFilePath(SimplexApp.context, tmpFile.name.replaceAfter(RecorderNative.extension, "")))
files.add((file)) withContext(Dispatchers.IO) {
chatModel.filesToDelete.remove(chosenAudioVal.first.toFile()) Files.move(tmpFile.toPath(), actualFile.toPath())
AudioPlayer.stop(chosenAudioVal.first.toFile().absolutePath)
msgs.add(MsgContent.MCVoice(if (msgs.isEmpty()) msgText else "", chosenAudioVal.second / 1000))
} }
files.add(actualFile.name)
deleteUnusedFiles()
msgs.add(MsgContent.MCVoice(if (msgs.isEmpty()) msgText else "", preview.durationMs / 1000))
} }
is ComposePreview.FilePreview -> { is ComposePreview.FilePreview -> {
val chosenFileVal = chosenFile.value val file = saveFileFromUri(context, preview.uri)
if (chosenFileVal != null) { if (file != null) {
val file = saveFileFromUri(context, chosenFileVal) files.add((file))
if (file != null) { msgs.add(MsgContent.MCFile(if (msgs.isEmpty()) msgText else ""))
files.add((file))
msgs.add(MsgContent.MCFile(if (msgs.isEmpty()) msgText else ""))
}
} }
} }
} }
@@ -487,7 +498,11 @@ fun ComposeView(
if (content !is MsgContent.MCVoice && index == msgs.lastIndex) live else false if (content !is MsgContent.MCVoice && index == msgs.lastIndex) live else false
) )
} }
if (sent == null && chosenContent.value.isNotEmpty()) { if (sent == null &&
(cs.preview is ComposePreview.MediaPreview ||
cs.preview is ComposePreview.FilePreview ||
cs.preview is ComposePreview.VoicePreview)
) {
sent = send(cInfo, MsgContent.MCText(msgText), quotedItemId, null, live) sent = send(cInfo, MsgContent.MCText(msgText), quotedItemId, null, live)
} }
} }
@@ -516,7 +531,6 @@ fun ComposeView(
fun onAudioAdded(filePath: String, durationMs: Int, finished: Boolean) { fun onAudioAdded(filePath: String, durationMs: Int, finished: Boolean) {
val file = File(filePath) val file = File(filePath)
chosenAudio.value = file.toUri() to durationMs
chatModel.filesToDelete.add(file) chatModel.filesToDelete.add(file)
composeState.value = composeState.value.copy(preview = ComposePreview.VoicePreview(filePath, durationMs, finished)) composeState.value = composeState.value.copy(preview = ComposePreview.VoicePreview(filePath, durationMs, finished))
} }
@@ -539,7 +553,6 @@ fun ComposeView(
fun cancelImages() { fun cancelImages() {
composeState.value = composeState.value.copy(preview = ComposePreview.NoPreview) composeState.value = composeState.value.copy(preview = ComposePreview.NoPreview)
chosenContent.value = emptyList()
} }
fun cancelVoice() { fun cancelVoice() {
@@ -551,12 +564,10 @@ fun ComposeView(
AudioPlayer.stop(filePath) AudioPlayer.stop(filePath)
filePath?.let { File(it).delete() } filePath?.let { File(it).delete() }
} }
chosenAudio.value = null
} }
fun cancelFile() { fun cancelFile() {
composeState.value = composeState.value.copy(preview = ComposePreview.NoPreview) composeState.value = composeState.value.copy(preview = ComposePreview.NoPreview)
chosenFile.value = null
} }
fun truncateToWords(s: String): String { fun truncateToWords(s: String): String {
@@ -613,8 +624,8 @@ fun ComposeView(
when (val preview = composeState.value.preview) { when (val preview = composeState.value.preview) {
ComposePreview.NoPreview -> {} ComposePreview.NoPreview -> {}
is ComposePreview.CLinkPreview -> ComposeLinkView(preview.linkPreview, ::cancelLinkPreview) is ComposePreview.CLinkPreview -> ComposeLinkView(preview.linkPreview, ::cancelLinkPreview)
is ComposePreview.ImagePreview -> ComposeImageView( is ComposePreview.MediaPreview -> ComposeImageView(
preview.images, preview,
::cancelImages, ::cancelImages,
cancelEnabled = !composeState.value.editing cancelEnabled = !composeState.value.editing
) )
@@ -637,10 +648,10 @@ fun ComposeView(
fun contextItemView() { fun contextItemView() {
when (val contextItem = composeState.value.contextItem) { when (val contextItem = composeState.value.contextItem) {
ComposeContextItem.NoContextItem -> {} ComposeContextItem.NoContextItem -> {}
is ComposeContextItem.QuotedItem -> ContextItemView(contextItem.chatItem, Icons.Outlined.Reply) { is ComposeContextItem.QuotedItem -> ContextItemView(contextItem.chatItem, painterResource(R.drawable.ic_reply)) {
composeState.value = composeState.value.copy(contextItem = ComposeContextItem.NoContextItem) composeState.value = composeState.value.copy(contextItem = ComposeContextItem.NoContextItem)
} }
is ComposeContextItem.EditingItem -> ContextItemView(contextItem.chatItem, Icons.Filled.Edit) { is ComposeContextItem.EditingItem -> ContextItemView(contextItem.chatItem, painterResource(R.drawable.ic_edit_filled)) {
clearState() clearState()
} }
} }
@@ -652,13 +663,16 @@ fun ComposeView(
when (val shared = chatModel.sharedContent.value) { when (val shared = chatModel.sharedContent.value) {
is SharedContent.Text -> onMessageChange(shared.text) is SharedContent.Text -> onMessageChange(shared.text)
is SharedContent.Images -> processPickedImage(shared.uris, shared.text) is SharedContent.Media -> processPickedMedia(shared.uris, shared.text)
is SharedContent.File -> processPickedFile(shared.uri, shared.text) is SharedContent.File -> processPickedFile(shared.uri, shared.text)
null -> {} null -> {}
} }
chatModel.sharedContent.value = null chatModel.sharedContent.value = null
} }
val userCanSend = rememberUpdatedState(chat.userCanSend)
val userIsObserver = rememberUpdatedState(chat.userIsObserver)
Column { Column {
contextItemView() contextItemView()
when { when {
@@ -670,11 +684,11 @@ fun ComposeView(
modifier = Modifier.padding(end = 8.dp), modifier = Modifier.padding(end = 8.dp),
verticalAlignment = Alignment.Bottom, verticalAlignment = Alignment.Bottom,
) { ) {
IconButton(showChooseAttachment, enabled = !composeState.value.attachmentDisabled) { IconButton(showChooseAttachment, enabled = !composeState.value.attachmentDisabled && rememberUpdatedState(chat.userCanSend).value) {
Icon( Icon(
Icons.Filled.AttachFile, painterResource(R.drawable.ic_attach_file_filled_500),
contentDescription = stringResource(R.string.attach), contentDescription = stringResource(R.string.attach),
tint = if (!composeState.value.attachmentDisabled) MaterialTheme.colors.primary else HighOrLowlight, tint = if (!composeState.value.attachmentDisabled && userCanSend.value) MaterialTheme.colors.primary else MaterialTheme.colors.secondary,
modifier = Modifier modifier = Modifier
.size(28.dp) .size(28.dp)
.clip(CircleShape) .clip(CircleShape)
@@ -682,7 +696,7 @@ fun ComposeView(
} }
val allowedVoiceByPrefs = remember(chat.chatInfo) { chat.chatInfo.featureEnabled(ChatFeature.Voice) } val allowedVoiceByPrefs = remember(chat.chatInfo) { chat.chatInfo.featureEnabled(ChatFeature.Voice) }
LaunchedEffect(allowedVoiceByPrefs) { LaunchedEffect(allowedVoiceByPrefs) {
if (!allowedVoiceByPrefs && chosenAudio.value != null) { if (!allowedVoiceByPrefs && composeState.value.preview is ComposePreview.VoicePreview) {
// Voice was disabled right when this user records it, just cancel it // Voice was disabled right when this user records it, just cancel it
cancelVoice() cancelVoice()
} }
@@ -705,6 +719,20 @@ fun ComposeView(
} }
} }
fun clearCurrentDraft() {
if (chatModel.draftChatId.value == chat.id) {
chatModel.draft.value = null
chatModel.draftChatId.value = null
}
}
LaunchedEffect(rememberUpdatedState(chat.userCanSend).value) {
if (!chat.userCanSend) {
clearCurrentDraft()
clearState()
}
}
val activity = LocalContext.current as Activity val activity = LocalContext.current as Activity
DisposableEffect(Unit) { DisposableEffect(Unit) {
val orientation = activity.resources.configuration.orientation val orientation = activity.resources.configuration.orientation
@@ -714,6 +742,17 @@ fun ComposeView(
if (cs.liveMessage != null && (cs.message.isNotEmpty() || cs.liveMessage.sent)) { if (cs.liveMessage != null && (cs.message.isNotEmpty() || cs.liveMessage.sent)) {
sendMessage() sendMessage()
resetLinkPreview() resetLinkPreview()
clearCurrentDraft()
deleteUnusedFiles()
} else if (!composeState.value.empty) {
if (cs.preview is ComposePreview.VoicePreview && !cs.preview.finished) {
composeState.value = cs.copy(preview = cs.preview.copy(finished = true))
}
chatModel.draft.value = composeState.value
chatModel.draftChatId.value = chat.id
} else {
clearCurrentDraft()
deleteUnusedFiles()
} }
chatModel.removeLiveDummy() chatModel.removeLiveDummy()
} }
@@ -729,6 +768,8 @@ fun ComposeView(
needToAllowVoiceToContact, needToAllowVoiceToContact,
allowedVoiceByPrefs, allowedVoiceByPrefs,
allowVoiceToContact = ::allowVoiceToContact, allowVoiceToContact = ::allowVoiceToContact,
userIsObserver = userIsObserver.value,
userCanSend = userCanSend.value,
sendMessage = { sendMessage = {
sendMessage() sendMessage()
resetLinkPreview() resetLinkPreview()
@@ -755,7 +796,7 @@ class PickFromGallery: ActivityResultContract<Int, Uri?>() {
override fun parseResult(resultCode: Int, intent: Intent?): Uri? = intent?.data override fun parseResult(resultCode: Int, intent: Intent?): Uri? = intent?.data
} }
class PickMultipleFromGallery: ActivityResultContract<Int, List<Uri>>() { class PickMultipleImagesFromGallery: ActivityResultContract<Int, List<Uri>>() {
override fun createIntent(context: Context, input: Int) = override fun createIntent(context: Context, input: Int) =
Intent(Intent.ACTION_PICK, MediaStore.Images.Media.INTERNAL_CONTENT_URI).apply { Intent(Intent.ACTION_PICK, MediaStore.Images.Media.INTERNAL_CONTENT_URI).apply {
putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true) putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
@@ -780,3 +821,30 @@ class PickMultipleFromGallery: ActivityResultContract<Int, List<Uri>>() {
else else
emptyList() emptyList()
} }
class PickMultipleVideosFromGallery: ActivityResultContract<Int, List<Uri>>() {
override fun createIntent(context: Context, input: Int) =
Intent(Intent.ACTION_PICK, MediaStore.Video.Media.INTERNAL_CONTENT_URI).apply {
putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
type = "video/*"
}
override fun parseResult(resultCode: Int, intent: Intent?): List<Uri> =
if (intent?.data != null)
listOf(intent.data!!)
else if (intent?.clipData != null)
with(intent.clipData!!) {
val uris = ArrayList<Uri>()
for (i in 0 until kotlin.math.min(itemCount, 10)) {
val uri = getItemAt(i).uri
if (uri != null) uris.add(uri)
}
if (itemCount > 10) {
AlertManager.shared.showAlertMsg(R.string.videos_limit_title, R.string.videos_limit_desc)
}
uris
}
else
emptyList()
}
@@ -2,13 +2,11 @@ import androidx.compose.animation.core.Animatable
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.material.* import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material.icons.outlined.Close
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@@ -16,7 +14,6 @@ import androidx.compose.ui.unit.sp
import chat.simplex.app.R import chat.simplex.app.R
import chat.simplex.app.model.durationText import chat.simplex.app.model.durationText
import chat.simplex.app.ui.theme.* import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.chat.item.SentColorLight
import chat.simplex.app.views.helpers.* import chat.simplex.app.views.helpers.*
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
@@ -59,12 +56,13 @@ fun ComposeVoiceView(
.height(3.dp) .height(3.dp)
.background(MaterialTheme.colors.primary) .background(MaterialTheme.colors.primary)
) )
val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage
Row( Row(
Modifier Modifier
.height(60.dp) .height(60.dp)
.fillMaxWidth() .fillMaxWidth()
.padding(top = 8.dp) .padding(top = 8.dp)
.background(SentColorLight), .background(sentColor),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
IconButton( IconButton(
@@ -77,12 +75,12 @@ fun ComposeVoiceView(
}, },
enabled = finishedRecording) { enabled = finishedRecording) {
Icon( Icon(
if (audioPlaying.value) Icons.Filled.Pause else Icons.Filled.PlayArrow, if (audioPlaying.value) painterResource(R.drawable.ic_pause_filled) else painterResource(R.drawable.ic_play_arrow_filled),
stringResource(R.string.icon_descr_file), stringResource(R.string.icon_descr_file),
Modifier Modifier
.padding(start = 4.dp, end = 2.dp) .padding(start = 4.dp, end = 2.dp)
.size(36.dp), .size(36.dp),
tint = if (finishedRecording) MaterialTheme.colors.primary else HighOrLowlight tint = if (finishedRecording) MaterialTheme.colors.primary else MaterialTheme.colors.secondary
) )
} }
val numberInText = remember(recordedDurationMs, progress.value) { val numberInText = remember(recordedDurationMs, progress.value) {
@@ -97,7 +95,7 @@ fun ComposeVoiceView(
Text( Text(
durationText(numberInText.value), durationText(numberInText.value),
fontSize = 18.sp, fontSize = 18.sp,
color = HighOrLowlight, color = MaterialTheme.colors.secondary,
) )
Spacer(Modifier.weight(1f)) Spacer(Modifier.weight(1f))
if (cancelEnabled) { if (cancelEnabled) {
@@ -109,7 +107,7 @@ fun ComposeVoiceView(
modifier = Modifier.padding(0.dp) modifier = Modifier.padding(0.dp)
) { ) {
Icon( Icon(
Icons.Outlined.Close, painterResource(R.drawable.ic_close),
contentDescription = stringResource(R.string.icon_descr_cancel_file_preview), contentDescription = stringResource(R.string.icon_descr_cancel_file_preview),
tint = MaterialTheme.colors.primary, tint = MaterialTheme.colors.primary,
modifier = Modifier.padding(10.dp) modifier = Modifier.padding(10.dp)
@@ -1,9 +1,9 @@
package chat.simplex.app.views.chat package chat.simplex.app.views.chat
import InfoRow import InfoRow
import SectionDivider import SectionBottomSpacer
import SectionDividerSpaced
import SectionItemView import SectionItemView
import SectionSpacer
import SectionTextFooter import SectionTextFooter
import SectionView import SectionView
import androidx.compose.foundation.* import androidx.compose.foundation.*
@@ -12,7 +12,6 @@ import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
@@ -50,7 +49,6 @@ fun ContactPreferencesView(
if (featuresAllowed == currentFeaturesAllowed) close() if (featuresAllowed == currentFeaturesAllowed) close()
else showUnsavedChangesAlert({ savePrefs(close) }, close) else showUnsavedChangesAlert({ savePrefs(close) }, close)
}, },
background = if (isInDarkTheme()) MaterialTheme.colors.background else SettingsBackgroundLight
) { ) {
ContactPreferencesLayout( ContactPreferencesLayout(
featuresAllowed, featuresAllowed,
@@ -81,34 +79,38 @@ private fun ContactPreferencesLayout(
Column( Column(
Modifier Modifier
.fillMaxWidth() .fillMaxWidth()
.verticalScroll(rememberScrollState()) .verticalScroll(rememberScrollState()),
.padding(bottom = DEFAULT_PADDING),
horizontalAlignment = Alignment.Start,
) { ) {
AppBarTitle(stringResource(R.string.contact_preferences)) AppBarTitle(stringResource(R.string.contact_preferences))
val timedMessages: MutableState<Boolean> = remember(featuresAllowed) { mutableStateOf(featuresAllowed.timedMessagesAllowed) } val timedMessages: MutableState<Boolean> = remember(featuresAllowed) { mutableStateOf(featuresAllowed.timedMessagesAllowed) }
val onTTLUpdated = { ttl: Int? -> val onTTLUpdated = { ttl: Int? ->
applyPrefs(featuresAllowed.copy(timedMessagesTTL = ttl ?: 86400)) applyPrefs(featuresAllowed.copy(timedMessagesTTL = ttl))
} }
TimedMessagesFeatureSection(featuresAllowed, contact.mergedPreferences.timedMessages, timedMessages, onTTLUpdated) { allowed, ttl -> TimedMessagesFeatureSection(featuresAllowed, contact.mergedPreferences.timedMessages, timedMessages, onTTLUpdated) { allowed, ttl ->
applyPrefs(featuresAllowed.copy(timedMessagesAllowed = allowed, timedMessagesTTL = ttl ?: currentFeaturesAllowed.timedMessagesTTL)) applyPrefs(featuresAllowed.copy(timedMessagesAllowed = allowed, timedMessagesTTL = ttl ?: currentFeaturesAllowed.timedMessagesTTL))
} }
SectionSpacer() SectionDividerSpaced(true, maxBottomPadding = false)
val allowFullDeletion: MutableState<ContactFeatureAllowed> = remember(featuresAllowed) { mutableStateOf(featuresAllowed.fullDelete) } val allowFullDeletion: MutableState<ContactFeatureAllowed> = remember(featuresAllowed) { mutableStateOf(featuresAllowed.fullDelete) }
FeatureSection(ChatFeature.FullDelete, user.fullPreferences.fullDelete.allow, contact.mergedPreferences.fullDelete, allowFullDeletion) { FeatureSection(ChatFeature.FullDelete, user.fullPreferences.fullDelete.allow, contact.mergedPreferences.fullDelete, allowFullDeletion) {
applyPrefs(featuresAllowed.copy(fullDelete = it)) applyPrefs(featuresAllowed.copy(fullDelete = it))
} }
SectionSpacer() SectionDividerSpaced(true, maxBottomPadding = false)
val allowVoice: MutableState<ContactFeatureAllowed> = remember(featuresAllowed) { mutableStateOf(featuresAllowed.voice) } val allowVoice: MutableState<ContactFeatureAllowed> = remember(featuresAllowed) { mutableStateOf(featuresAllowed.voice) }
FeatureSection(ChatFeature.Voice, user.fullPreferences.voice.allow, contact.mergedPreferences.voice, allowVoice) { FeatureSection(ChatFeature.Voice, user.fullPreferences.voice.allow, contact.mergedPreferences.voice, allowVoice) {
applyPrefs(featuresAllowed.copy(voice = it)) applyPrefs(featuresAllowed.copy(voice = it))
} }
SectionSpacer() SectionDividerSpaced(true, maxBottomPadding = false)
val allowCalls: MutableState<ContactFeatureAllowed> = remember(featuresAllowed) { mutableStateOf(featuresAllowed.calls) }
FeatureSection(ChatFeature.Calls, user.fullPreferences.calls.allow, contact.mergedPreferences.calls, allowCalls) {
applyPrefs(featuresAllowed.copy(calls = it))
}
SectionDividerSpaced(maxTopPadding = true, maxBottomPadding = false)
ResetSaveButtons( ResetSaveButtons(
reset = reset, reset = reset,
save = savePrefs, save = savePrefs,
disabled = featuresAllowed == currentFeaturesAllowed disabled = featuresAllowed == currentFeaturesAllowed
) )
SectionBottomSpacer()
} }
} }
@@ -128,26 +130,24 @@ private fun FeatureSection(
SectionView( SectionView(
feature.text.uppercase(), feature.text.uppercase(),
icon = feature.iconFilled, icon = feature.iconFilled(),
iconTint = if (enabled.forUser) SimplexGreen else if (enabled.forContact) WarningYellow else Color.Red, iconTint = if (enabled.forUser) SimplexGreen else if (enabled.forContact) WarningYellow else Color.Red,
leadingIcon = true, leadingIcon = true,
) { ) {
SectionItemView { ExposedDropDownSettingRow(
ExposedDropDownSettingRow( generalGetString(R.string.chat_preferences_you_allow),
generalGetString(R.string.chat_preferences_you_allow), ContactFeatureAllowed.values(userDefault).map { it to it.text },
ContactFeatureAllowed.values(userDefault).map { it to it.text }, allowFeature,
allowFeature, icon = null,
icon = null, enabled = remember { mutableStateOf(feature != ChatFeature.Calls) },
onSelected = onSelected onSelected = onSelected
) )
}
SectionDivider()
InfoRow( InfoRow(
generalGetString(R.string.chat_preferences_contact_allows), generalGetString(R.string.chat_preferences_contact_allows),
pref.contactPreference.allow.text pref.contactPreference.allow.text
) )
} }
SectionTextFooter(feature.enabledDescription(enabled)) SectionTextFooter(feature.enabledDescription(enabled) + (if (feature == ChatFeature.Calls) generalGetString(R.string.available_in_v51) else ""))
} }
@Composable @Composable
@@ -166,24 +166,20 @@ private fun TimedMessagesFeatureSection(
SectionView( SectionView(
ChatFeature.TimedMessages.text.uppercase(), ChatFeature.TimedMessages.text.uppercase(),
icon = ChatFeature.TimedMessages.iconFilled, icon = ChatFeature.TimedMessages.iconFilled(),
iconTint = if (enabled.forUser) SimplexGreen else if (enabled.forContact) WarningYellow else Color.Red, iconTint = if (enabled.forUser) SimplexGreen else if (enabled.forContact) WarningYellow else Color.Red,
leadingIcon = true, leadingIcon = true,
) { ) {
SectionItemView { PreferenceToggle(
PreferenceToggle( generalGetString(R.string.chat_preferences_you_allow),
generalGetString(R.string.chat_preferences_you_allow), checked = allowFeature.value,
checked = allowFeature.value, ) { allow ->
) { allow -> onSelected(allow, if (allow) featuresAllowed.timedMessagesTTL ?: 86400 else null)
onSelected(allow, if (allow) featuresAllowed.timedMessagesTTL ?: 86400 else null)
}
} }
SectionDivider()
InfoRow( InfoRow(
generalGetString(R.string.chat_preferences_contact_allows), generalGetString(R.string.chat_preferences_contact_allows),
pref.contactPreference.allow.text pref.contactPreference.allow.text
) )
SectionDivider()
if (featuresAllowed.timedMessagesAllowed) { if (featuresAllowed.timedMessagesAllowed) {
val ttl = rememberSaveable(featuresAllowed.timedMessagesTTL) { mutableStateOf(featuresAllowed.timedMessagesTTL) } val ttl = rememberSaveable(featuresAllowed.timedMessagesTTL) { mutableStateOf(featuresAllowed.timedMessagesTTL) }
TimedMessagesTTLPicker(ttl, onTTLUpdated) TimedMessagesTTLPicker(ttl, onTTLUpdated)
@@ -198,11 +194,10 @@ private fun TimedMessagesFeatureSection(
private fun ResetSaveButtons(reset: () -> Unit, save: () -> Unit, disabled: Boolean) { private fun ResetSaveButtons(reset: () -> Unit, save: () -> Unit, disabled: Boolean) {
SectionView { SectionView {
SectionItemView(reset, disabled = disabled) { SectionItemView(reset, disabled = disabled) {
Text(stringResource(R.string.reset_verb), color = if (disabled) HighOrLowlight else MaterialTheme.colors.primary) Text(stringResource(R.string.reset_verb), color = if (disabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary)
} }
SectionDivider()
SectionItemView(save, disabled = disabled) { SectionItemView(save, disabled = disabled) {
Text(stringResource(R.string.save_and_notify_contact), color = if (disabled) HighOrLowlight else MaterialTheme.colors.primary) Text(stringResource(R.string.save_and_notify_contact), color = if (disabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary)
} }
} }
} }
@@ -211,14 +206,12 @@ private fun ResetSaveButtons(reset: () -> Unit, save: () -> Unit, disabled: Bool
fun TimedMessagesTTLPicker(selection: MutableState<Int?>, onSelected: (Int?) -> Unit) { fun TimedMessagesTTLPicker(selection: MutableState<Int?>, onSelected: (Int?) -> Unit) {
val ttlValues = TimedMessagesPreference.ttlValues val ttlValues = TimedMessagesPreference.ttlValues
val values = ttlValues + if (ttlValues.contains(selection.value)) listOf() else listOf(selection.value) val values = ttlValues + if (ttlValues.contains(selection.value)) listOf() else listOf(selection.value)
SectionItemView { ExposedDropDownSettingRow(
ExposedDropDownSettingRow( generalGetString(R.string.delete_after),
generalGetString(R.string.delete_after), values.map { it to TimedMessagesPreference.ttlText(it) },
values.map { it to TimedMessagesPreference.ttlText(it) }, selection,
selection, onSelected = onSelected
onSelected = onSelected )
)
}
} }
private fun showUnsavedChangesAlert(save: () -> Unit, revert: () -> Unit) { private fun showUnsavedChangesAlert(save: () -> Unit, revert: () -> Unit) {
@@ -3,34 +3,34 @@ package chat.simplex.app.views.chat
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.material.* import androidx.compose.material.*
import androidx.compose.material.icons.Icons import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material.icons.outlined.Close
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import chat.simplex.app.R import chat.simplex.app.R
import chat.simplex.app.model.* import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.HighOrLowlight import chat.simplex.app.ui.theme.*
import chat.simplex.app.ui.theme.SimpleXTheme
import chat.simplex.app.views.chat.item.* import chat.simplex.app.views.chat.item.*
import kotlinx.datetime.Clock import kotlinx.datetime.Clock
@Composable @Composable
fun ContextItemView( fun ContextItemView(
contextItem: ChatItem, contextItem: ChatItem,
contextIcon: ImageVector, contextIcon: Painter,
cancelContextItem: () -> Unit cancelContextItem: () -> Unit
) { ) {
val sent = contextItem.chatDir.sent val sent = contextItem.chatDir.sent
val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage
val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage
Row( Row(
Modifier Modifier
.padding(top = 8.dp) .padding(top = 8.dp)
.background(if (sent) SentColorLight else ReceivedColorLight), .background(if (sent) sentColor else receivedColor),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Row( Row(
@@ -47,7 +47,7 @@ fun ContextItemView(
.height(20.dp) .height(20.dp)
.width(20.dp), .width(20.dp),
contentDescription = stringResource(R.string.icon_descr_context), contentDescription = stringResource(R.string.icon_descr_context),
tint = HighOrLowlight, tint = MaterialTheme.colors.secondary,
) )
MarkdownText( MarkdownText(
contextItem.text, contextItem.formattedText, contextItem.text, contextItem.formattedText,
@@ -58,7 +58,7 @@ fun ContextItemView(
} }
IconButton(onClick = cancelContextItem) { IconButton(onClick = cancelContextItem) {
Icon( Icon(
Icons.Outlined.Close, painterResource(R.drawable.ic_close),
contentDescription = stringResource(R.string.cancel_verb), contentDescription = stringResource(R.string.cancel_verb),
tint = MaterialTheme.colors.primary, tint = MaterialTheme.colors.primary,
modifier = Modifier.padding(10.dp) modifier = Modifier.padding(10.dp)
@@ -73,8 +73,7 @@ fun PreviewContextItemView() {
SimpleXTheme { SimpleXTheme {
ContextItemView( ContextItemView(
contextItem = ChatItem.getSampleData(1, CIDirection.DirectRcv(), Clock.System.now(), "hello"), contextItem = ChatItem.getSampleData(1, CIDirection.DirectRcv(), Clock.System.now(), "hello"),
contextIcon = Icons.Filled.Edit, contextIcon = painterResource(R.drawable.ic_edit_filled)
cancelContextItem = {} ) {}
)
} }
} }
@@ -8,19 +8,18 @@ import android.content.pm.ActivityInfo
import android.content.res.Configuration import android.content.res.Configuration
import android.os.Build import android.os.Build
import android.text.InputType import android.text.InputType
import android.util.Log
import android.view.ViewGroup import android.view.ViewGroup
import android.view.WindowManager import android.view.WindowManager
import android.view.inputmethod.* import android.view.inputmethod.*
import android.widget.EditText import android.widget.EditText
import android.widget.TextView
import androidx.compose.animation.core.* import androidx.compose.animation.core.*
import androidx.compose.foundation.* import androidx.compose.foundation.*
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.* import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material.icons.outlined.*
import androidx.compose.material.ripple.rememberRipple import androidx.compose.material.ripple.rememberRipple
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
@@ -28,8 +27,9 @@ import androidx.compose.ui.*
import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.* import androidx.compose.ui.graphics.*
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.platform.* import androidx.compose.ui.platform.*
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.Role
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
@@ -44,12 +44,13 @@ import androidx.core.widget.*
import chat.simplex.app.R import chat.simplex.app.R
import chat.simplex.app.SimplexApp import chat.simplex.app.SimplexApp
import chat.simplex.app.model.* import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.HighOrLowlight import chat.simplex.app.ui.theme.CurrentColors
import chat.simplex.app.ui.theme.SimpleXTheme import chat.simplex.app.ui.theme.SimpleXTheme
import chat.simplex.app.views.chat.item.ItemAction import chat.simplex.app.views.chat.item.ItemAction
import chat.simplex.app.views.helpers.* import chat.simplex.app.views.helpers.*
import com.google.accompanist.permissions.rememberMultiplePermissionsState import com.google.accompanist.permissions.rememberMultiplePermissionsState
import kotlinx.coroutines.* import kotlinx.coroutines.*
import java.lang.reflect.Field
@Composable @Composable
fun SendMsgView( fun SendMsgView(
@@ -60,6 +61,8 @@ fun SendMsgView(
liveMessageAlertShown: SharedPreference<Boolean>, liveMessageAlertShown: SharedPreference<Boolean>,
needToAllowVoiceToContact: Boolean, needToAllowVoiceToContact: Boolean,
allowedVoiceByPrefs: Boolean, allowedVoiceByPrefs: Boolean,
userIsObserver: Boolean,
userCanSend: Boolean,
allowVoiceToContact: () -> Unit, allowVoiceToContact: () -> Unit,
sendMessage: () -> Unit, sendMessage: () -> Unit,
sendLiveMessage: (suspend () -> Unit)? = null, sendLiveMessage: (suspend () -> Unit)? = null,
@@ -70,13 +73,25 @@ fun SendMsgView(
) { ) {
Box(Modifier.padding(vertical = 8.dp)) { Box(Modifier.padding(vertical = 8.dp)) {
val cs = composeState.value val cs = composeState.value
val showProgress = cs.inProgress && (cs.preview is ComposePreview.ImagePreview || cs.preview is ComposePreview.FilePreview) val showProgress = cs.inProgress && (cs.preview is ComposePreview.MediaPreview || cs.preview is ComposePreview.FilePreview)
val showVoiceButton = cs.message.isEmpty() && showVoiceRecordIcon && !composeState.value.editing && val showVoiceButton = cs.message.isEmpty() && showVoiceRecordIcon && !composeState.value.editing &&
cs.liveMessage == null && (cs.preview is ComposePreview.NoPreview || recState.value is RecordingState.Started) cs.liveMessage == null && (cs.preview is ComposePreview.NoPreview || recState.value is RecordingState.Started)
NativeKeyboard(composeState, textStyle, onMessageChange) val showDeleteTextButton = rememberSaveable { mutableStateOf(false) }
NativeKeyboard(composeState, textStyle, showDeleteTextButton, userIsObserver, onMessageChange)
// Disable clicks on text field // Disable clicks on text field
if (cs.preview is ComposePreview.VoicePreview) { if (cs.preview is ComposePreview.VoicePreview || !userCanSend) {
Box(Modifier.matchParentSize().clickable(enabled = false, onClick = { })) Box(Modifier
.matchParentSize()
.clickable(enabled = !userCanSend, indication = null, interactionSource = remember { MutableInteractionSource() }, onClick = {
AlertManager.shared.showAlertMsg(
title = generalGetString(R.string.observer_cant_send_message_title),
text = generalGetString(R.string.observer_cant_send_message_desc)
)
})
)
}
if (showDeleteTextButton.value) {
DeleteTextButton(composeState)
} }
Box(Modifier.align(Alignment.BottomEnd)) { Box(Modifier.align(Alignment.BottomEnd)) {
val sendButtonSize = remember { Animatable(36f) } val sendButtonSize = remember { Animatable(36f) }
@@ -95,11 +110,11 @@ fun SendMsgView(
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
val stopRecOnNextClick = remember { mutableStateOf(false) } val stopRecOnNextClick = remember { mutableStateOf(false) }
when { when {
needToAllowVoiceToContact || !allowedVoiceByPrefs -> { needToAllowVoiceToContact || !allowedVoiceByPrefs || !userCanSend -> {
DisallowedVoiceButton { DisallowedVoiceButton(userCanSend) {
if (needToAllowVoiceToContact) { if (needToAllowVoiceToContact) {
showNeedToAllowVoiceAlert(allowVoiceToContact) showNeedToAllowVoiceAlert(allowVoiceToContact)
} else { } else if (!allowedVoiceByPrefs) {
showDisabledVoiceAlert(isDirectChat) showDisabledVoiceAlert(isDirectChat)
} }
} }
@@ -114,7 +129,7 @@ fun SendMsgView(
&& (cs.preview !is ComposePreview.VoicePreview || !stopRecOnNextClick.value) && (cs.preview !is ComposePreview.VoicePreview || !stopRecOnNextClick.value)
&& cs.contextItem is ComposeContextItem.NoContextItem) { && cs.contextItem is ComposeContextItem.NoContextItem) {
Spacer(Modifier.width(10.dp)) Spacer(Modifier.width(10.dp))
StartLiveMessageButton { StartLiveMessageButton(userCanSend) {
if (composeState.value.preview is ComposePreview.NoPreview) { if (composeState.value.preview is ComposePreview.NoPreview) {
startLiveMessage(scope, sendLiveMessage, updateLiveMessage, sendButtonSize, sendButtonAlpha, composeState, liveMessageAlertShown) startLiveMessage(scope, sendLiveMessage, updateLiveMessage, sendButtonSize, sendButtonAlpha, composeState, liveMessageAlertShown)
} }
@@ -129,7 +144,7 @@ fun SendMsgView(
} }
else -> { else -> {
val cs = composeState.value val cs = composeState.value
val icon = if (cs.editing || cs.liveMessage != null) Icons.Filled.Check else Icons.Outlined.ArrowUpward val icon = if (cs.editing || cs.liveMessage != null) painterResource(R.drawable.ic_check_filled) else painterResource(R.drawable.ic_arrow_upward)
val disabled = !cs.sendEnabled() || val disabled = !cs.sendEnabled() ||
(!allowedVoiceByPrefs && cs.preview is ComposePreview.VoicePreview) || (!allowedVoiceByPrefs && cs.preview is ComposePreview.VoicePreview) ||
cs.endLiveDisabled cs.endLiveDisabled
@@ -138,20 +153,18 @@ fun SendMsgView(
cs.contextItem is ComposeContextItem.NoContextItem && cs.contextItem is ComposeContextItem.NoContextItem &&
sendLiveMessage != null && updateLiveMessage != null sendLiveMessage != null && updateLiveMessage != null
) { ) {
var showDropdown by rememberSaveable { mutableStateOf(false) } val showDropdown = rememberSaveable { mutableStateOf(false) }
SendMsgButton(icon, sendButtonSize, sendButtonAlpha, !disabled, sendMessage) { showDropdown = true } SendMsgButton(icon, sendButtonSize, sendButtonAlpha, !disabled, sendMessage) { showDropdown.value = true }
DropdownMenu( DefaultDropdownMenu(
expanded = showDropdown, showDropdown,
onDismissRequest = { showDropdown = false },
Modifier.width(220.dp),
) { ) {
ItemAction( ItemAction(
generalGetString(R.string.send_live_message), generalGetString(R.string.send_live_message),
Icons.Filled.Bolt, BoltFilled,
onClick = { onClick = {
startLiveMessage(scope, sendLiveMessage, updateLiveMessage, sendButtonSize, sendButtonAlpha, composeState, liveMessageAlertShown) startLiveMessage(scope, sendLiveMessage, updateLiveMessage, sendButtonSize, sendButtonAlpha, composeState, liveMessageAlertShown)
showDropdown = false showDropdown.value = false
} }
) )
} }
@@ -168,11 +181,13 @@ fun SendMsgView(
private fun NativeKeyboard( private fun NativeKeyboard(
composeState: MutableState<ComposeState>, composeState: MutableState<ComposeState>,
textStyle: MutableState<TextStyle>, textStyle: MutableState<TextStyle>,
showDeleteTextButton: MutableState<Boolean>,
userIsObserver: Boolean,
onMessageChange: (String) -> Unit onMessageChange: (String) -> Unit
) { ) {
val cs = composeState.value val cs = composeState.value
val textColor = MaterialTheme.colors.onBackground val textColor = MaterialTheme.colors.onBackground
val tintColor = MaterialTheme.colors.secondary val tintColor = MaterialTheme.colors.secondaryVariant
val padding = PaddingValues(12.dp, 7.dp, 45.dp, 0.dp) val padding = PaddingValues(12.dp, 7.dp, 45.dp, 0.dp)
val paddingStart = with(LocalDensity.current) { 12.dp.roundToPx() } val paddingStart = with(LocalDensity.current) { 12.dp.roundToPx() }
val paddingTop = with(LocalDensity.current) { 7.dp.roundToPx() } val paddingTop = with(LocalDensity.current) { 7.dp.roundToPx() }
@@ -208,7 +223,7 @@ private fun NativeKeyboard(
} catch (e: Exception) { } catch (e: Exception) {
return@OnCommitContentListener false return@OnCommitContentListener false
} }
SimplexApp.context.chatModel.sharedContent.value = SharedContent.Images("", listOf(inputContentInfo.contentUri)) SimplexApp.context.chatModel.sharedContent.value = SharedContent.Media("", listOf(inputContentInfo.contentUri))
true true
} }
return InputConnectionCompat.createWrapper(connection, editorInfo, onCommit) return InputConnectionCompat.createWrapper(connection, editorInfo, onCommit)
@@ -224,7 +239,17 @@ private fun NativeKeyboard(
editText.background = drawable editText.background = drawable
editText.setPadding(paddingStart, paddingTop, paddingEnd, paddingBottom) editText.setPadding(paddingStart, paddingTop, paddingEnd, paddingBottom)
editText.setText(cs.message) editText.setText(cs.message)
editText.textCursorDrawable?.let { DrawableCompat.setTint(it, HighOrLowlight.toArgb()) } if (Build.VERSION.SDK_INT >= 29) {
editText.textCursorDrawable?.let { DrawableCompat.setTint(it, CurrentColors.value.colors.secondary.toArgb()) }
} else {
try {
val f: Field = TextView::class.java.getDeclaredField("mCursorDrawableRes")
f.isAccessible = true
f.set(editText, R.drawable.edit_text_cursor)
} catch (e: Exception) {
Log.e(chat.simplex.app.TAG, e.stackTraceToString())
}
}
editText.doOnTextChanged { text, _, _, _ -> onMessageChange(text.toString()) } editText.doOnTextChanged { text, _, _, _ -> onMessageChange(text.toString()) }
editText.doAfterTextChanged { text -> if (composeState.value.preview is ComposePreview.VoicePreview && text.toString() != "") editText.setText("") } editText.doAfterTextChanged { text -> if (composeState.value.preview is ComposePreview.VoicePreview && text.toString() != "") editText.setText("") }
editText editText
@@ -245,14 +270,32 @@ private fun NativeKeyboard(
imm.showSoftInput(it, InputMethodManager.SHOW_IMPLICIT) imm.showSoftInput(it, InputMethodManager.SHOW_IMPLICIT)
showKeyboard = false showKeyboard = false
} }
showDeleteTextButton.value = it.lineCount >= 4
} }
if (composeState.value.preview is ComposePreview.VoicePreview) { if (composeState.value.preview is ComposePreview.VoicePreview) {
Text( ComposeOverlay(R.string.voice_message_send_text, textStyle, padding)
generalGetString(R.string.voice_message_send_text), } else if (userIsObserver) {
Modifier.padding(padding), ComposeOverlay(R.string.you_are_observer, textStyle, padding)
color = HighOrLowlight, }
style = textStyle.value.copy(fontStyle = FontStyle.Italic) }
)
@Composable
private fun ComposeOverlay(textId: Int, textStyle: MutableState<TextStyle>, padding: PaddingValues) {
Text(
generalGetString(textId),
Modifier.padding(padding),
color = MaterialTheme.colors.secondary,
style = textStyle.value.copy(fontStyle = FontStyle.Italic)
)
}
@Composable
private fun BoxScope.DeleteTextButton(composeState: MutableState<ComposeState>) {
IconButton(
{ composeState.value = composeState.value.copy(message = "") },
Modifier.align(Alignment.TopEnd).size(36.dp)
) {
Icon(painterResource(R.drawable.ic_close), null, Modifier.padding(7.dp).size(36.dp), tint = MaterialTheme.colors.secondary)
} }
} }
@@ -306,12 +349,12 @@ private fun RecordVoiceView(recState: MutableState<RecordingState>, stopRecOnNex
} }
@Composable @Composable
private fun DisallowedVoiceButton(onClick: () -> Unit) { private fun DisallowedVoiceButton(enabled: Boolean, onClick: () -> Unit) {
IconButton(onClick, Modifier.size(36.dp)) { IconButton(onClick, Modifier.size(36.dp), enabled = enabled) {
Icon( Icon(
Icons.Outlined.KeyboardVoice, painterResource(R.drawable.ic_keyboard_voice),
stringResource(R.string.icon_descr_record_voice_message), stringResource(R.string.icon_descr_record_voice_message),
tint = HighOrLowlight, tint = MaterialTheme.colors.secondary,
modifier = Modifier modifier = Modifier
.size(36.dp) .size(36.dp)
.padding(4.dp) .padding(4.dp)
@@ -323,7 +366,7 @@ private fun DisallowedVoiceButton(onClick: () -> Unit) {
private fun VoiceButtonWithoutPermission(onClick: () -> Unit) { private fun VoiceButtonWithoutPermission(onClick: () -> Unit) {
IconButton(onClick, Modifier.size(36.dp)) { IconButton(onClick, Modifier.size(36.dp)) {
Icon( Icon(
Icons.Filled.KeyboardVoice, painterResource(R.drawable.ic_keyboard_voice_filled),
stringResource(R.string.icon_descr_record_voice_message), stringResource(R.string.icon_descr_record_voice_message),
tint = MaterialTheme.colors.primary, tint = MaterialTheme.colors.primary,
modifier = Modifier modifier = Modifier
@@ -355,7 +398,7 @@ private fun LockToCurrentOrientationUntilDispose() {
private fun StopRecordButton(onClick: () -> Unit) { private fun StopRecordButton(onClick: () -> Unit) {
IconButton(onClick, Modifier.size(36.dp)) { IconButton(onClick, Modifier.size(36.dp)) {
Icon( Icon(
Icons.Filled.Stop, painterResource(R.drawable.ic_stop_filled),
stringResource(R.string.icon_descr_record_voice_message), stringResource(R.string.icon_descr_record_voice_message),
tint = MaterialTheme.colors.primary, tint = MaterialTheme.colors.primary,
modifier = Modifier modifier = Modifier
@@ -369,7 +412,7 @@ private fun StopRecordButton(onClick: () -> Unit) {
private fun RecordVoiceButton(interactionSource: MutableInteractionSource) { private fun RecordVoiceButton(interactionSource: MutableInteractionSource) {
IconButton({}, Modifier.size(36.dp), interactionSource = interactionSource) { IconButton({}, Modifier.size(36.dp), interactionSource = interactionSource) {
Icon( Icon(
Icons.Filled.KeyboardVoice, painterResource(R.drawable.ic_keyboard_voice_filled),
stringResource(R.string.icon_descr_record_voice_message), stringResource(R.string.icon_descr_record_voice_message),
tint = MaterialTheme.colors.primary, tint = MaterialTheme.colors.primary,
modifier = Modifier modifier = Modifier
@@ -381,7 +424,7 @@ private fun RecordVoiceButton(interactionSource: MutableInteractionSource) {
@Composable @Composable
private fun ProgressIndicator() { private fun ProgressIndicator() {
CircularProgressIndicator(Modifier.size(36.dp).padding(4.dp), color = HighOrLowlight, strokeWidth = 3.dp) CircularProgressIndicator(Modifier.size(36.dp).padding(4.dp), color = MaterialTheme.colors.secondary, strokeWidth = 3.dp)
} }
@Composable @Composable
@@ -390,7 +433,7 @@ private fun CancelLiveMessageButton(
) { ) {
IconButton(onClick, Modifier.size(36.dp)) { IconButton(onClick, Modifier.size(36.dp)) {
Icon( Icon(
Icons.Filled.Close, painterResource(R.drawable.ic_close),
stringResource(R.string.icon_descr_cancel_live_message), stringResource(R.string.icon_descr_cancel_live_message),
tint = MaterialTheme.colors.primary, tint = MaterialTheme.colors.primary,
modifier = Modifier modifier = Modifier
@@ -402,7 +445,7 @@ private fun CancelLiveMessageButton(
@Composable @Composable
private fun SendMsgButton( private fun SendMsgButton(
icon: ImageVector, icon: Painter,
sizeDp: Animatable<Float, AnimationVector1D>, sizeDp: Animatable<Float, AnimationVector1D>,
alpha: Animatable<Float, AnimationVector1D>, alpha: Animatable<Float, AnimationVector1D>,
enabled: Boolean, enabled: Boolean,
@@ -431,20 +474,20 @@ private fun SendMsgButton(
.padding(4.dp) .padding(4.dp)
.alpha(alpha.value) .alpha(alpha.value)
.clip(CircleShape) .clip(CircleShape)
.background(if (enabled) MaterialTheme.colors.primary else HighOrLowlight) .background(if (enabled) MaterialTheme.colors.primary else MaterialTheme.colors.secondary)
.padding(3.dp) .padding(3.dp)
) )
} }
} }
@Composable @Composable
private fun StartLiveMessageButton(onClick: () -> Unit) { private fun StartLiveMessageButton(enabled: Boolean, onClick: () -> Unit) {
val interactionSource = remember { MutableInteractionSource() } val interactionSource = remember { MutableInteractionSource() }
Box( Box(
modifier = Modifier.requiredSize(36.dp) modifier = Modifier.requiredSize(36.dp)
.clickable( .clickable(
onClick = onClick, onClick = onClick,
enabled = true, enabled = enabled,
role = Role.Button, role = Role.Button,
interactionSource = interactionSource, interactionSource = interactionSource,
indication = rememberRipple(bounded = false, radius = 24.dp) indication = rememberRipple(bounded = false, radius = 24.dp)
@@ -452,9 +495,9 @@ private fun StartLiveMessageButton(onClick: () -> Unit) {
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
Icon( Icon(
Icons.Filled.Bolt, BoltFilled,
stringResource(R.string.icon_descr_send_message), stringResource(R.string.icon_descr_send_message),
tint = MaterialTheme.colors.primary, tint = if (enabled) MaterialTheme.colors.primary else MaterialTheme.colors.secondary,
modifier = Modifier modifier = Modifier
.size(36.dp) .size(36.dp)
.padding(4.dp) .padding(4.dp)
@@ -555,6 +598,8 @@ fun PreviewSendMsgView() {
liveMessageAlertShown = SharedPreference(get = { true }, set = { }), liveMessageAlertShown = SharedPreference(get = { true }, set = { }),
needToAllowVoiceToContact = false, needToAllowVoiceToContact = false,
allowedVoiceByPrefs = true, allowedVoiceByPrefs = true,
userIsObserver = false,
userCanSend = true,
allowVoiceToContact = {}, allowVoiceToContact = {},
sendMessage = {}, sendMessage = {},
onMessageChange = { _ -> }, onMessageChange = { _ -> },
@@ -583,6 +628,8 @@ fun PreviewSendMsgViewEditing() {
liveMessageAlertShown = SharedPreference(get = { true }, set = { }), liveMessageAlertShown = SharedPreference(get = { true }, set = { }),
needToAllowVoiceToContact = false, needToAllowVoiceToContact = false,
allowedVoiceByPrefs = true, allowedVoiceByPrefs = true,
userIsObserver = false,
userCanSend = true,
allowVoiceToContact = {}, allowVoiceToContact = {},
sendMessage = {}, sendMessage = {},
onMessageChange = { _ -> }, onMessageChange = { _ -> },
@@ -601,7 +648,7 @@ fun PreviewSendMsgViewEditing() {
fun PreviewSendMsgViewInProgress() { fun PreviewSendMsgViewInProgress() {
val smallFont = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.onBackground) val smallFont = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.onBackground)
val textStyle = remember { mutableStateOf(smallFont) } val textStyle = remember { mutableStateOf(smallFont) }
val composeStateInProgress = ComposeState(preview = ComposePreview.FilePreview("test.txt"), inProgress = true, useLinkPreviews = true) val composeStateInProgress = ComposeState(preview = ComposePreview.FilePreview("test.txt", getAppFileUri("test.txt")), inProgress = true, useLinkPreviews = true)
SimpleXTheme { SimpleXTheme {
SendMsgView( SendMsgView(
composeState = remember { mutableStateOf(composeStateInProgress) }, composeState = remember { mutableStateOf(composeStateInProgress) },
@@ -611,6 +658,8 @@ fun PreviewSendMsgViewInProgress() {
liveMessageAlertShown = SharedPreference(get = { true }, set = { }), liveMessageAlertShown = SharedPreference(get = { true }, set = { }),
needToAllowVoiceToContact = false, needToAllowVoiceToContact = false,
allowedVoiceByPrefs = true, allowedVoiceByPrefs = true,
userIsObserver = false,
userCanSend = true,
allowVoiceToContact = {}, allowVoiceToContact = {},
sendMessage = {}, sendMessage = {},
onMessageChange = { _ -> }, onMessageChange = { _ -> },
@@ -1,18 +1,17 @@
package chat.simplex.app.views.chat package chat.simplex.app.views.chat
import SectionBottomSpacer
import SectionView import SectionView
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.* import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Share
import androidx.compose.material.icons.outlined.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@@ -66,7 +65,7 @@ private fun VerifyCodeLayout(
val splitCode = splitToParts(connectionCode, 24) val splitCode = splitToParts(connectionCode, 24)
Row(Modifier.fillMaxWidth().padding(bottom = DEFAULT_PADDING_HALF), horizontalArrangement = Arrangement.Center) { Row(Modifier.fillMaxWidth().padding(bottom = DEFAULT_PADDING_HALF), horizontalArrangement = Arrangement.Center) {
if (connectionVerified) { if (connectionVerified) {
Icon(Icons.Outlined.VerifiedUser, null, Modifier.padding(end = 4.dp).size(22.dp), tint = HighOrLowlight) Icon(painterResource(R.drawable.ic_verified_user), null, Modifier.padding(end = 4.dp).size(22.dp), tint = MaterialTheme.colors.secondary)
Text(String.format(stringResource(R.string.is_verified), displayName)) Text(String.format(stringResource(R.string.is_verified), displayName))
} else { } else {
Text(String.format(stringResource(R.string.is_not_verified), displayName)) Text(String.format(stringResource(R.string.is_not_verified), displayName))
@@ -90,7 +89,7 @@ private fun VerifyCodeLayout(
val context = LocalContext.current val context = LocalContext.current
Box(Modifier.weight(1f)) { Box(Modifier.weight(1f)) {
IconButton({ shareText(context, connectionCode) }, Modifier.size(20.dp).align(Alignment.CenterStart)) { IconButton({ shareText(context, connectionCode) }, Modifier.size(20.dp).align(Alignment.CenterStart)) {
Icon(Icons.Filled.Share, null, tint = MaterialTheme.colors.primary) Icon(painterResource(R.drawable.ic_share_filled), null, tint = MaterialTheme.colors.primary)
} }
} }
Spacer(Modifier.weight(1f)) Spacer(Modifier.weight(1f))
@@ -106,16 +105,16 @@ private fun VerifyCodeLayout(
horizontalArrangement = Arrangement.spacedBy(10.dp) horizontalArrangement = Arrangement.spacedBy(10.dp)
) { ) {
if (connectionVerified) { if (connectionVerified) {
SimpleButton(generalGetString(R.string.clear_verification), Icons.Outlined.Shield) { SimpleButton(generalGetString(R.string.clear_verification), painterResource(R.drawable.ic_shield)) {
verifyCode(null) {} verifyCode(null) {}
} }
} else { } else {
SimpleButton(generalGetString(R.string.scan_code), Icons.Outlined.QrCode) { SimpleButton(generalGetString(R.string.scan_code), painterResource(R.drawable.ic_qr_code)) {
ModalManager.shared.showModal { ModalManager.shared.showModal {
ScanCodeView(verifyCode) { } ScanCodeView(verifyCode) { }
} }
} }
SimpleButton(generalGetString(R.string.mark_code_verified), Icons.Outlined.VerifiedUser) { SimpleButton(generalGetString(R.string.mark_code_verified), painterResource(R.drawable.ic_verified_user)) {
verifyCode(connectionCode) { verified -> verifyCode(connectionCode) { verified ->
if (!verified) { if (!verified) {
AlertManager.shared.showAlertMsg( AlertManager.shared.showAlertMsg(
@@ -126,6 +125,7 @@ private fun VerifyCodeLayout(
} }
} }
} }
SectionBottomSpacer()
} }
} }
@@ -134,4 +134,4 @@ private fun splitToParts(s: String, length: Int): String {
return (0..(s.length - 1) / length) return (0..(s.length - 1) / length)
.map { s.drop(it * length).take(length) } .map { s.drop(it * length).take(length) }
.joinToString(separator = "\n") .joinToString(separator = "\n")
} }
@@ -1,7 +1,8 @@
package chat.simplex.app.views.chat.group package chat.simplex.app.views.chat.group
import SectionBottomSpacer
import SectionCustomFooter import SectionCustomFooter
import SectionDivider import SectionDividerSpaced
import SectionItemView import SectionItemView
import SectionSpacer import SectionSpacer
import SectionView import SectionView
@@ -9,15 +10,12 @@ import androidx.activity.compose.BackHandler
import androidx.compose.foundation.* import androidx.compose.foundation.*
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.material.* import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.CheckCircle
import androidx.compose.material.icons.filled.TheaterComedy
import androidx.compose.material.icons.outlined.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
@@ -28,6 +26,7 @@ import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.* import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.chat.ChatInfoToolbarTitle import chat.simplex.app.views.chat.ChatInfoToolbarTitle
import chat.simplex.app.views.helpers.* import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.newchat.InfoAboutIncognito
import chat.simplex.app.views.usersettings.SettingsActionItem import chat.simplex.app.views.usersettings.SettingsActionItem
@Composable @Composable
@@ -37,6 +36,7 @@ fun AddGroupMembersView(groupInfo: GroupInfo, creatingGroup: Boolean = false, ch
var allowModifyMembers by remember { mutableStateOf(true) } var allowModifyMembers by remember { mutableStateOf(true) }
BackHandler(onBack = close) BackHandler(onBack = close)
AddGroupMembersLayout( AddGroupMembersLayout(
chatModel.incognito.value,
groupInfo = groupInfo, groupInfo = groupInfo,
creatingGroup = creatingGroup, creatingGroup = creatingGroup,
contactsToAdd = getContactsToAdd(chatModel), contactsToAdd = getContactsToAdd(chatModel),
@@ -85,6 +85,7 @@ fun getContactsToAdd(chatModel: ChatModel): List<Contact> {
@Composable @Composable
fun AddGroupMembersLayout( fun AddGroupMembersLayout(
chatModelIncognito: Boolean,
groupInfo: GroupInfo, groupInfo: GroupInfo,
creatingGroup: Boolean, creatingGroup: Boolean,
contactsToAdd: List<Contact>, contactsToAdd: List<Contact>,
@@ -102,9 +103,16 @@ fun AddGroupMembersLayout(
Modifier Modifier
.fillMaxWidth() .fillMaxWidth()
.verticalScroll(rememberScrollState()), .verticalScroll(rememberScrollState()),
horizontalAlignment = Alignment.Start,
) { ) {
AppBarTitle(stringResource(R.string.button_add_members)) AppBarTitle(stringResource(R.string.button_add_members))
InfoAboutIncognito(
chatModelIncognito,
false,
generalGetString(R.string.group_unsupported_incognito_main_profile_sent),
generalGetString(R.string.group_main_profile_sent),
true
)
Spacer(Modifier.size(DEFAULT_PADDING))
Row( Row(
Modifier.fillMaxWidth(), Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center horizontalArrangement = Arrangement.Center
@@ -125,7 +133,7 @@ fun AddGroupMembersLayout(
Text( Text(
stringResource(R.string.no_contacts_to_add), stringResource(R.string.no_contacts_to_add),
Modifier.padding(), Modifier.padding(),
color = HighOrLowlight color = MaterialTheme.colors.secondary
) )
} }
} else { } else {
@@ -134,12 +142,8 @@ fun AddGroupMembersLayout(
SectionItemView(openPreferences) { SectionItemView(openPreferences) {
Text(stringResource(R.string.set_group_preferences)) Text(stringResource(R.string.set_group_preferences))
} }
SectionDivider()
} }
SectionItemView { RoleSelectionRow(groupInfo, selectedRole, allowModifyMembers)
RoleSelectionRow(groupInfo, selectedRole, allowModifyMembers)
}
SectionDivider()
if (creatingGroup && selectedContacts.isEmpty()) { if (creatingGroup && selectedContacts.isEmpty()) {
SkipInvitingButton(close) SkipInvitingButton(close)
} else { } else {
@@ -149,13 +153,13 @@ fun AddGroupMembersLayout(
SectionCustomFooter { SectionCustomFooter {
InviteSectionFooter(selectedContactsCount = selectedContacts.size, allowModifyMembers, clearSelection) InviteSectionFooter(selectedContactsCount = selectedContacts.size, allowModifyMembers, clearSelection)
} }
SectionSpacer() SectionDividerSpaced(maxTopPadding = true)
SectionView(stringResource(R.string.select_contacts)) { SectionView(stringResource(R.string.select_contacts)) {
ContactList(contacts = contactsToAdd, selectedContacts, groupInfo, allowModifyMembers, addContact, removeContact) ContactList(contacts = contactsToAdd, selectedContacts, groupInfo, allowModifyMembers, addContact, removeContact)
} }
SectionSpacer()
} }
SectionBottomSpacer()
} }
} }
@@ -172,16 +176,15 @@ private fun RoleSelectionRow(groupInfo: GroupInfo, selectedRole: MutableState<Gr
values, values,
selectedRole, selectedRole,
icon = null, icon = null,
enabled = rememberUpdatedState(enabled), enabled = rememberUpdatedState(enabled)
onSelected = { selectedRole.value = it } ) { selectedRole.value = it }
)
} }
} }
@Composable @Composable
fun InviteMembersButton(onClick: () -> Unit, disabled: Boolean) { fun InviteMembersButton(onClick: () -> Unit, disabled: Boolean) {
SettingsActionItem( SettingsActionItem(
Icons.Outlined.Check, painterResource(R.drawable.ic_check),
stringResource(R.string.invite_to_group_button), stringResource(R.string.invite_to_group_button),
click = onClick, click = onClick,
textColor = MaterialTheme.colors.primary, textColor = MaterialTheme.colors.primary,
@@ -193,7 +196,7 @@ fun InviteMembersButton(onClick: () -> Unit, disabled: Boolean) {
@Composable @Composable
fun SkipInvitingButton(onClick: () -> Unit) { fun SkipInvitingButton(onClick: () -> Unit) {
SettingsActionItem( SettingsActionItem(
Icons.Outlined.Check, painterResource(R.drawable.ic_check),
stringResource(R.string.skip_inviting_button), stringResource(R.string.skip_inviting_button),
click = onClick, click = onClick,
textColor = MaterialTheme.colors.primary, textColor = MaterialTheme.colors.primary,
@@ -211,7 +214,7 @@ fun InviteSectionFooter(selectedContactsCount: Int, enabled: Boolean, clearSelec
if (selectedContactsCount >= 1) { if (selectedContactsCount >= 1) {
Text( Text(
String.format(generalGetString(R.string.num_contacts_selected), selectedContactsCount), String.format(generalGetString(R.string.num_contacts_selected), selectedContactsCount),
color = HighOrLowlight, color = MaterialTheme.colors.secondary,
fontSize = 12.sp fontSize = 12.sp
) )
Box( Box(
@@ -219,14 +222,14 @@ fun InviteSectionFooter(selectedContactsCount: Int, enabled: Boolean, clearSelec
) { ) {
Text( Text(
stringResource(R.string.clear_contacts_selection_button), stringResource(R.string.clear_contacts_selection_button),
color = if (enabled) MaterialTheme.colors.primary else HighOrLowlight, color = if (enabled) MaterialTheme.colors.primary else MaterialTheme.colors.secondary,
fontSize = 12.sp fontSize = 12.sp
) )
} }
} else { } else {
Text( Text(
stringResource(R.string.no_contacts_selected), stringResource(R.string.no_contacts_selected),
color = HighOrLowlight, color = MaterialTheme.colors.secondary,
fontSize = 12.sp fontSize = 12.sp
) )
} }
@@ -249,9 +252,6 @@ fun ContactList(
checked = selectedContacts.contains(contact.apiId), checked = selectedContacts.contains(contact.apiId),
enabled = enabled, enabled = enabled,
) )
if (index < contacts.lastIndex) {
SectionDivider()
}
} }
} }
} }
@@ -266,17 +266,17 @@ fun ContactCheckRow(
enabled: Boolean, enabled: Boolean,
) { ) {
val prohibitedToInviteIncognito = !groupInfo.membership.memberIncognito && contact.contactConnIncognito val prohibitedToInviteIncognito = !groupInfo.membership.memberIncognito && contact.contactConnIncognito
val icon: ImageVector val icon: Painter
val iconColor: Color val iconColor: Color
if (prohibitedToInviteIncognito) { if (prohibitedToInviteIncognito) {
icon = Icons.Filled.TheaterComedy icon = painterResource(R.drawable.ic_theater_comedy_filled)
iconColor = HighOrLowlight iconColor = MaterialTheme.colors.secondary
} else if (checked) { } else if (checked) {
icon = Icons.Filled.CheckCircle icon = painterResource(R.drawable.ic_check_circle_filled)
iconColor = if (enabled) MaterialTheme.colors.primary else HighOrLowlight iconColor = if (enabled) MaterialTheme.colors.primary else MaterialTheme.colors.secondary
} else { } else {
icon = Icons.Outlined.Circle icon = painterResource(R.drawable.ic_circle)
iconColor = HighOrLowlight iconColor = MaterialTheme.colors.secondary
} }
SectionItemView( SectionItemView(
click = if (enabled) { click = if (enabled) {
@@ -294,7 +294,7 @@ fun ContactCheckRow(
Spacer(Modifier.width(DEFAULT_SPACE_AFTER_ICON)) Spacer(Modifier.width(DEFAULT_SPACE_AFTER_ICON))
Text( Text(
contact.chatViewName, maxLines = 1, overflow = TextOverflow.Ellipsis, contact.chatViewName, maxLines = 1, overflow = TextOverflow.Ellipsis,
color = if (prohibitedToInviteIncognito) HighOrLowlight else Color.Unspecified color = if (prohibitedToInviteIncognito) MaterialTheme.colors.secondary else Color.Unspecified
) )
Spacer(Modifier.fillMaxWidth().weight(1f)) Spacer(Modifier.fillMaxWidth().weight(1f))
Icon( Icon(
@@ -318,6 +318,7 @@ fun showProhibitedToInviteIncognitoAlertDialog() {
fun PreviewAddGroupMembersLayout() { fun PreviewAddGroupMembersLayout() {
SimpleXTheme { SimpleXTheme {
AddGroupMembersLayout( AddGroupMembersLayout(
chatModelIncognito = false,
groupInfo = GroupInfo.sampleData, groupInfo = GroupInfo.sampleData,
creatingGroup = false, creatingGroup = false,
contactsToAdd = listOf(Contact.sampleData, Contact.sampleData, Contact.sampleData), contactsToAdd = listOf(Contact.sampleData, Contact.sampleData, Contact.sampleData),
@@ -1,7 +1,8 @@
package chat.simplex.app.views.chat.group package chat.simplex.app.views.chat.group
import InfoRow import InfoRow
import SectionDivider import SectionBottomSpacer
import SectionDividerSpaced
import SectionItemView import SectionItemView
import SectionSpacer import SectionSpacer
import SectionTextFooter import SectionTextFooter
@@ -11,12 +12,11 @@ import androidx.activity.compose.BackHandler
import androidx.compose.foundation.* import androidx.compose.foundation.*
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.material.* import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
@@ -34,7 +34,7 @@ import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.usersettings.* import chat.simplex.app.views.usersettings.*
@Composable @Composable
fun GroupChatInfoView(chatModel: ChatModel, groupLink: String?, onGroupLinkUpdated: (String?) -> Unit, close: () -> Unit) { fun GroupChatInfoView(chatModel: ChatModel, groupLink: String?, groupLinkMemberRole: GroupMemberRole?, onGroupLinkUpdated: (Pair<String?, GroupMemberRole?>) -> Unit, close: () -> Unit) {
BackHandler(onBack = close) BackHandler(onBack = close)
val chat = chatModel.chats.firstOrNull { it.id == chatModel.chatId.value } val chat = chatModel.chats.firstOrNull { it.id == chatModel.chatId.value }
val developerTools = chatModel.controller.appPrefs.developerTools.get() val developerTools = chatModel.controller.appPrefs.developerTools.get()
@@ -82,6 +82,9 @@ fun GroupChatInfoView(chatModel: ChatModel, groupLink: String?, onGroupLinkUpdat
editGroupProfile = { editGroupProfile = {
ModalManager.shared.showCustomModal { close -> GroupProfileView(groupInfo, chatModel, close) } ModalManager.shared.showCustomModal { close -> GroupProfileView(groupInfo, chatModel, close) }
}, },
addOrEditWelcomeMessage = {
ModalManager.shared.showCustomModal { close -> GroupWelcomeView(chatModel, groupInfo, close) }
},
openPreferences = { openPreferences = {
ModalManager.shared.showCustomModal { close -> ModalManager.shared.showCustomModal { close ->
GroupPreferencesView( GroupPreferencesView(
@@ -95,9 +98,7 @@ fun GroupChatInfoView(chatModel: ChatModel, groupLink: String?, onGroupLinkUpdat
clearChat = { clearChatDialog(chat.chatInfo, chatModel, close) }, clearChat = { clearChatDialog(chat.chatInfo, chatModel, close) },
leaveGroup = { leaveGroupDialog(groupInfo, chatModel, close) }, leaveGroup = { leaveGroupDialog(groupInfo, chatModel, close) },
manageGroupLink = { manageGroupLink = {
withApi { ModalManager.shared.showModal { GroupLinkView(chatModel, groupInfo, groupLink, groupLinkMemberRole, onGroupLinkUpdated) }
ModalManager.shared.showModal { GroupLinkView(chatModel, groupInfo, groupLink, onGroupLinkUpdated) }
}
} }
) )
} }
@@ -107,7 +108,7 @@ fun deleteGroupDialog(chatInfo: ChatInfo, groupInfo: GroupInfo, chatModel: ChatM
val alertTextKey = val alertTextKey =
if (groupInfo.membership.memberCurrent) R.string.delete_group_for_all_members_cannot_undo_warning if (groupInfo.membership.memberCurrent) R.string.delete_group_for_all_members_cannot_undo_warning
else R.string.delete_group_for_self_cannot_undo_warning else R.string.delete_group_for_self_cannot_undo_warning
AlertManager.shared.showAlertMsg( AlertManager.shared.showAlertDialog(
title = generalGetString(R.string.delete_group_question), title = generalGetString(R.string.delete_group_question),
text = generalGetString(alertTextKey), text = generalGetString(alertTextKey),
confirmText = generalGetString(R.string.delete_verb), confirmText = generalGetString(R.string.delete_verb),
@@ -121,12 +122,13 @@ fun deleteGroupDialog(chatInfo: ChatInfo, groupInfo: GroupInfo, chatModel: ChatM
close?.invoke() close?.invoke()
} }
} }
} },
destructive = true,
) )
} }
fun leaveGroupDialog(groupInfo: GroupInfo, chatModel: ChatModel, close: (() -> Unit)? = null) { fun leaveGroupDialog(groupInfo: GroupInfo, chatModel: ChatModel, close: (() -> Unit)? = null) {
AlertManager.shared.showAlertMsg( AlertManager.shared.showAlertDialog(
title = generalGetString(R.string.leave_group_question), title = generalGetString(R.string.leave_group_question),
text = generalGetString(R.string.you_will_stop_receiving_messages_from_this_group_chat_history_will_be_preserved), text = generalGetString(R.string.you_will_stop_receiving_messages_from_this_group_chat_history_will_be_preserved),
confirmText = generalGetString(R.string.leave_group_button), confirmText = generalGetString(R.string.leave_group_button),
@@ -135,7 +137,8 @@ fun leaveGroupDialog(groupInfo: GroupInfo, chatModel: ChatModel, close: (() -> U
chatModel.controller.leaveGroup(groupInfo.groupId) chatModel.controller.leaveGroup(groupInfo.groupId)
close?.invoke() close?.invoke()
} }
} },
destructive = true,
) )
} }
@@ -149,6 +152,7 @@ fun GroupChatInfoLayout(
addMembers: () -> Unit, addMembers: () -> Unit,
showMemberInfo: (GroupMember) -> Unit, showMemberInfo: (GroupMember) -> Unit,
editGroupProfile: () -> Unit, editGroupProfile: () -> Unit,
addOrEditWelcomeMessage: () -> Unit,
openPreferences: () -> Unit, openPreferences: () -> Unit,
deleteGroup: () -> Unit, deleteGroup: () -> Unit,
clearChat: () -> Unit, clearChat: () -> Unit,
@@ -158,8 +162,7 @@ fun GroupChatInfoLayout(
Column( Column(
Modifier Modifier
.fillMaxWidth() .fillMaxWidth()
.verticalScroll(rememberScrollState()), .verticalScroll(rememberScrollState())
horizontalAlignment = Alignment.Start
) { ) {
Row( Row(
Modifier.fillMaxWidth(), Modifier.fillMaxWidth(),
@@ -171,61 +174,50 @@ fun GroupChatInfoLayout(
SectionView { SectionView {
if (groupInfo.canEdit) { if (groupInfo.canEdit) {
SectionItemView(editGroupProfile) { EditGroupProfileButton() } EditGroupProfileButton(editGroupProfile)
SectionDivider() AddOrEditWelcomeMessage(groupInfo.groupProfile.description, addOrEditWelcomeMessage)
} }
GroupPreferencesButton(openPreferences) GroupPreferencesButton(openPreferences)
} }
SectionTextFooter(stringResource(R.string.only_group_owners_can_change_prefs)) SectionTextFooter(stringResource(R.string.only_group_owners_can_change_prefs))
SectionSpacer() SectionDividerSpaced(maxTopPadding = true)
SectionView(title = String.format(generalGetString(R.string.group_info_section_title_num_members), members.count() + 1)) { SectionView(title = String.format(generalGetString(R.string.group_info_section_title_num_members), members.count() + 1)) {
if (groupInfo.canAddMembers) { if (groupInfo.canAddMembers) {
SectionItemView(manageGroupLink) { if (groupLink == null) {
if (groupLink == null) { CreateGroupLinkButton(manageGroupLink)
CreateGroupLinkButton() } else {
} else { GroupLinkButton(manageGroupLink)
GroupLinkButton()
}
} }
SectionDivider()
val onAddMembersClick = if (chat.chatInfo.incognito) ::cantInviteIncognitoAlert else addMembers val onAddMembersClick = if (chat.chatInfo.incognito) ::cantInviteIncognitoAlert else addMembers
SectionItemView(onAddMembersClick) { val tint = if (chat.chatInfo.incognito) MaterialTheme.colors.secondary else MaterialTheme.colors.primary
val tint = if (chat.chatInfo.incognito) HighOrLowlight else MaterialTheme.colors.primary AddMembersButton(tint, onAddMembersClick)
AddMembersButton(tint)
}
SectionDivider()
} }
SectionItemView(minHeight = 50.dp) { SectionItemView(minHeight = 54.dp) {
MemberRow(groupInfo.membership, user = true) MemberRow(groupInfo.membership, user = true)
} }
if (members.isNotEmpty()) {
SectionDivider()
}
MembersList(members, showMemberInfo) MembersList(members, showMemberInfo)
} }
SectionSpacer() SectionDividerSpaced(maxTopPadding = true, maxBottomPadding = false)
SectionView { SectionView {
ClearChatButton(clearChat) ClearChatButton(clearChat)
if (groupInfo.canDelete) { if (groupInfo.canDelete) {
SectionDivider() DeleteGroupButton(deleteGroup)
SectionItemView(deleteGroup) { DeleteGroupButton() }
} }
if (groupInfo.membership.memberCurrent) { if (groupInfo.membership.memberCurrent) {
SectionDivider() LeaveGroupButton(leaveGroup)
SectionItemView(leaveGroup) { LeaveGroupButton() }
} }
} }
SectionSpacer()
if (developerTools) { if (developerTools) {
SectionDividerSpaced()
SectionView(title = stringResource(R.string.section_title_for_console)) { SectionView(title = stringResource(R.string.section_title_for_console)) {
InfoRow(stringResource(R.string.info_row_local_name), groupInfo.localDisplayName) InfoRow(stringResource(R.string.info_row_local_name), groupInfo.localDisplayName)
SectionDivider()
InfoRow(stringResource(R.string.info_row_database_id), groupInfo.apiId.toString()) InfoRow(stringResource(R.string.info_row_database_id), groupInfo.apiId.toString())
} }
SectionSpacer()
} }
SectionBottomSpacer()
} }
} }
@@ -256,38 +248,31 @@ private fun GroupChatInfoHeader(cInfo: ChatInfo) {
@Composable @Composable
private fun GroupPreferencesButton(onClick: () -> Unit) { private fun GroupPreferencesButton(onClick: () -> Unit) {
SettingsActionItem( SettingsActionItem(
Icons.Outlined.ToggleOn, painterResource(R.drawable.ic_toggle_on),
stringResource(R.string.group_preferences), stringResource(R.string.group_preferences),
click = onClick click = onClick
) )
} }
@Composable @Composable
private fun AddMembersButton(tint: Color = MaterialTheme.colors.primary) { private fun AddMembersButton(tint: Color = MaterialTheme.colors.primary, onClick: () -> Unit) {
Row( SettingsActionItem(
Modifier.fillMaxSize(), painterResource(R.drawable.ic_add),
verticalAlignment = Alignment.CenterVertically stringResource(R.string.button_add_members),
) { onClick,
Icon( iconColor = tint,
Icons.Outlined.Add, textColor = tint
stringResource(R.string.button_add_members), )
tint = tint
)
Spacer(Modifier.size(8.dp))
Text(stringResource(R.string.button_add_members), color = tint)
}
} }
@Composable @Composable
private fun MembersList(members: List<GroupMember>, showMemberInfo: (GroupMember) -> Unit) { private fun MembersList(members: List<GroupMember>, showMemberInfo: (GroupMember) -> Unit) {
Column { Column {
members.forEachIndexed { index, member -> members.forEachIndexed { index, member ->
SectionItemView({ showMemberInfo(member) }, minHeight = 50.dp) { Divider()
SectionItemView({ showMemberInfo(member) }, minHeight = 54.dp) {
MemberRow(member) MemberRow(member)
} }
if (index < members.lastIndex) {
SectionDivider()
}
} }
} }
} }
@@ -300,10 +285,12 @@ private fun MemberRow(member: GroupMember, user: Boolean = false) {
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Row( Row(
Modifier.weight(1f).padding(end = DEFAULT_PADDING),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp) horizontalArrangement = Arrangement.spacedBy(4.dp)
) { ) {
ProfileImage(size = 46.dp, member.image) ProfileImage(size = 46.dp, member.image)
Spacer(Modifier.width(DEFAULT_PADDING_HALF))
Column { Column {
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
if (member.verified) { if (member.verified) {
@@ -318,7 +305,7 @@ private fun MemberRow(member: GroupMember, user: Boolean = false) {
val statusDescr = if (user) String.format(generalGetString(R.string.group_info_member_you), s) else s val statusDescr = if (user) String.format(generalGetString(R.string.group_info_member_you), s) else s
Text( Text(
statusDescr, statusDescr,
color = HighOrLowlight, color = MaterialTheme.colors.secondary,
fontSize = 12.sp, fontSize = 12.sp,
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis overflow = TextOverflow.Ellipsis
@@ -327,97 +314,81 @@ private fun MemberRow(member: GroupMember, user: Boolean = false) {
} }
val role = member.memberRole val role = member.memberRole
if (role == GroupMemberRole.Owner || role == GroupMemberRole.Admin) { if (role == GroupMemberRole.Owner || role == GroupMemberRole.Admin) {
Text(role.text, color = HighOrLowlight) Text(role.text, color = MaterialTheme.colors.secondary)
} }
} }
} }
@Composable @Composable
private fun MemberVerifiedShield() { private fun MemberVerifiedShield() {
Icon(Icons.Outlined.VerifiedUser, null, Modifier.padding(end = 3.dp).size(16.dp), tint = HighOrLowlight) Icon(painterResource(R.drawable.ic_verified_user), null, Modifier.padding(end = 3.dp).size(16.dp), tint = MaterialTheme.colors.secondary)
} }
@Composable @Composable
private fun GroupLinkButton() { private fun GroupLinkButton(onClick: () -> Unit) {
Row( SettingsActionItem(
Modifier painterResource(R.drawable.ic_link),
.fillMaxSize(), stringResource(R.string.group_link),
verticalAlignment = Alignment.CenterVertically onClick,
) { iconColor = MaterialTheme.colors.secondary
Icon( )
Icons.Outlined.Link,
stringResource(R.string.group_link),
tint = HighOrLowlight
)
Spacer(Modifier.size(8.dp))
Text(stringResource(R.string.group_link))
}
} }
@Composable @Composable
private fun CreateGroupLinkButton() { private fun CreateGroupLinkButton(onClick: () -> Unit) {
Row( SettingsActionItem(
Modifier painterResource(R.drawable.ic_add_link),
.fillMaxSize(), stringResource(R.string.create_group_link),
verticalAlignment = Alignment.CenterVertically onClick,
) { iconColor = MaterialTheme.colors.secondary
Icon( )
Icons.Outlined.AddLink,
stringResource(R.string.create_group_link),
tint = HighOrLowlight
)
Spacer(Modifier.size(8.dp))
Text(stringResource(R.string.create_group_link))
}
} }
@Composable @Composable
fun EditGroupProfileButton() { fun EditGroupProfileButton(onClick: () -> Unit) {
Row( SettingsActionItem(
Modifier painterResource(R.drawable.ic_edit),
.fillMaxSize(), stringResource(R.string.button_edit_group_profile),
verticalAlignment = Alignment.CenterVertically onClick,
) { iconColor = MaterialTheme.colors.secondary
Icon( )
Icons.Outlined.Edit,
stringResource(R.string.button_edit_group_profile),
tint = HighOrLowlight
)
Spacer(Modifier.size(8.dp))
Text(stringResource(R.string.button_edit_group_profile))
}
} }
@Composable @Composable
private fun LeaveGroupButton() { private fun AddOrEditWelcomeMessage(welcomeMessage: String?, onClick: () -> Unit) {
Row( val text = if (welcomeMessage == null) {
Modifier.fillMaxSize(), stringResource(R.string.button_add_welcome_message)
verticalAlignment = Alignment.CenterVertically } else {
) { stringResource(R.string.button_welcome_message)
Icon(
Icons.Outlined.Logout,
stringResource(R.string.button_leave_group),
tint = Color.Red
)
Spacer(Modifier.size(8.dp))
Text(stringResource(R.string.button_leave_group), color = Color.Red)
} }
SettingsActionItem(
painterResource(R.drawable.ic_maps_ugc),
text,
onClick,
iconColor = MaterialTheme.colors.secondary
)
} }
@Composable @Composable
private fun DeleteGroupButton() { private fun LeaveGroupButton(onClick: () -> Unit) {
Row( SettingsActionItem(
Modifier.fillMaxSize(), painterResource(R.drawable.ic_logout),
verticalAlignment = Alignment.CenterVertically stringResource(R.string.button_leave_group),
) { onClick,
Icon( iconColor = Color.Red,
Icons.Outlined.Delete, textColor = Color.Red
stringResource(R.string.button_delete_group), )
tint = Color.Red }
)
Spacer(Modifier.size(8.dp)) @Composable
Text(stringResource(R.string.button_delete_group), color = Color.Red) private fun DeleteGroupButton(onClick: () -> Unit) {
} SettingsActionItem(
painterResource(R.drawable.ic_delete),
stringResource(R.string.button_delete_group),
onClick,
iconColor = Color.Red,
textColor = Color.Red
)
} }
@Preview @Preview
@@ -427,14 +398,13 @@ fun PreviewGroupChatInfoLayout() {
GroupChatInfoLayout( GroupChatInfoLayout(
chat = Chat( chat = Chat(
chatInfo = ChatInfo.Direct.sampleData, chatInfo = ChatInfo.Direct.sampleData,
chatItems = arrayListOf(), chatItems = arrayListOf()
serverInfo = Chat.ServerInfo(Chat.NetworkStatus.Error("agent BROKER TIMEOUT"))
), ),
groupInfo = GroupInfo.sampleData, groupInfo = GroupInfo.sampleData,
members = listOf(GroupMember.sampleData, GroupMember.sampleData, GroupMember.sampleData), members = listOf(GroupMember.sampleData, GroupMember.sampleData, GroupMember.sampleData),
developerTools = false, developerTools = false,
groupLink = null, groupLink = null,
addMembers = {}, showMemberInfo = {}, editGroupProfile = {}, openPreferences = {}, deleteGroup = {}, clearChat = {}, leaveGroup = {}, manageGroupLink = {}, addMembers = {}, showMemberInfo = {}, editGroupProfile = {}, addOrEditWelcomeMessage = {}, openPreferences = {}, deleteGroup = {}, clearChat = {}, leaveGroup = {}, manageGroupLink = {},
) )
} }
} }
@@ -1,36 +1,41 @@
package chat.simplex.app.views.chat.group package chat.simplex.app.views.chat.group
import SectionBottomSpacer
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.material.CircularProgressIndicator import androidx.compose.foundation.rememberScrollState
import androidx.compose.material.Text import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.*
import androidx.compose.material.icons.outlined.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import chat.simplex.app.R import chat.simplex.app.R
import chat.simplex.app.model.ChatModel import chat.simplex.app.model.*
import chat.simplex.app.model.GroupInfo
import chat.simplex.app.ui.theme.* import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.helpers.* import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.newchat.QRCode import chat.simplex.app.views.newchat.QRCode
@Composable @Composable
fun GroupLinkView(chatModel: ChatModel, groupInfo: GroupInfo, connReqContact: String?, onGroupLinkUpdated: (String?) -> Unit) { fun GroupLinkView(chatModel: ChatModel, groupInfo: GroupInfo, connReqContact: String?, memberRole: GroupMemberRole?, onGroupLinkUpdated: (Pair<String?, GroupMemberRole?>) -> Unit) {
var groupLink by rememberSaveable { mutableStateOf(connReqContact) } var groupLink by rememberSaveable { mutableStateOf(connReqContact) }
val groupLinkMemberRole = rememberSaveable { mutableStateOf(memberRole) }
var creatingLink by rememberSaveable { mutableStateOf(false) } var creatingLink by rememberSaveable { mutableStateOf(false) }
val cxt = LocalContext.current val cxt = LocalContext.current
fun createLink() { fun createLink() {
creatingLink = true creatingLink = true
withApi { withApi {
groupLink = chatModel.controller.apiCreateGroupLink(groupInfo.groupId) val link = chatModel.controller.apiCreateGroupLink(groupInfo.groupId)
onGroupLinkUpdated(groupLink) if (link != null) {
groupLink = link.first
groupLinkMemberRole.value = link.second
onGroupLinkUpdated(groupLink to groupLinkMemberRole.value)
}
creatingLink = false creatingLink = false
} }
} }
@@ -41,11 +46,26 @@ fun GroupLinkView(chatModel: ChatModel, groupInfo: GroupInfo, connReqContact: St
} }
GroupLinkLayout( GroupLinkLayout(
groupLink = groupLink, groupLink = groupLink,
groupInfo,
groupLinkMemberRole,
creatingLink, creatingLink,
createLink = ::createLink, createLink = ::createLink,
share = { shareText(cxt, groupLink ?: return@GroupLinkLayout) }, share = { shareText(cxt, groupLink ?: return@GroupLinkLayout) },
updateLink = {
val role = groupLinkMemberRole.value
if (role != null) {
withBGApi {
val link = chatModel.controller.apiGroupLinkMemberRole(groupInfo.groupId, role)
if (link != null) {
groupLink = link.first
groupLinkMemberRole.value = link.second
onGroupLinkUpdated(groupLink to groupLinkMemberRole.value)
}
}
}
},
deleteLink = { deleteLink = {
AlertManager.shared.showAlertMsg( AlertManager.shared.showAlertDialog(
title = generalGetString(R.string.delete_link_question), title = generalGetString(R.string.delete_link_question),
text = generalGetString(R.string.all_group_members_will_remain_connected), text = generalGetString(R.string.all_group_members_will_remain_connected),
confirmText = generalGetString(R.string.delete_verb), confirmText = generalGetString(R.string.delete_verb),
@@ -54,10 +74,11 @@ fun GroupLinkView(chatModel: ChatModel, groupInfo: GroupInfo, connReqContact: St
val r = chatModel.controller.apiDeleteGroupLink(groupInfo.groupId) val r = chatModel.controller.apiDeleteGroupLink(groupInfo.groupId)
if (r) { if (r) {
groupLink = null groupLink = null
onGroupLinkUpdated(null) onGroupLinkUpdated(null to null)
} }
} }
} },
destructive = true,
) )
} }
) )
@@ -69,20 +90,22 @@ fun GroupLinkView(chatModel: ChatModel, groupInfo: GroupInfo, connReqContact: St
@Composable @Composable
fun GroupLinkLayout( fun GroupLinkLayout(
groupLink: String?, groupLink: String?,
groupInfo: GroupInfo,
groupLinkMemberRole: MutableState<GroupMemberRole?>,
creatingLink: Boolean, creatingLink: Boolean,
createLink: () -> Unit, createLink: () -> Unit,
share: () -> Unit, share: () -> Unit,
updateLink: () -> Unit,
deleteLink: () -> Unit deleteLink: () -> Unit
) { ) {
Column( Column(
Modifier.padding(horizontal = DEFAULT_PADDING), Modifier
horizontalAlignment = Alignment.Start, .verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.Top
) { ) {
AppBarTitle(stringResource(R.string.group_link), false) AppBarTitle(stringResource(R.string.group_link))
Text( Text(
stringResource(R.string.you_can_share_group_link_anybody_will_be_able_to_connect), stringResource(R.string.you_can_share_group_link_anybody_will_be_able_to_connect),
Modifier.padding(bottom = 12.dp), Modifier.padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, bottom = 12.dp),
lineHeight = 22.sp lineHeight = 22.sp
) )
Column( Column(
@@ -91,28 +114,55 @@ fun GroupLinkLayout(
verticalArrangement = Arrangement.SpaceEvenly verticalArrangement = Arrangement.SpaceEvenly
) { ) {
if (groupLink == null) { if (groupLink == null) {
SimpleButton(stringResource(R.string.button_create_group_link), icon = Icons.Outlined.AddLink, disabled = creatingLink, click = createLink) SimpleButton(stringResource(R.string.button_create_group_link), icon = painterResource(R.drawable.ic_add_link), disabled = creatingLink, click = createLink)
} else { } else {
QRCode(groupLink, Modifier.weight(1f, fill = false).aspectRatio(1f)) RoleSelectionRow(groupInfo, groupLinkMemberRole)
var initialLaunch by remember { mutableStateOf(true) }
LaunchedEffect(groupLinkMemberRole.value) {
if (!initialLaunch) {
updateLink()
}
initialLaunch = false
}
QRCode(groupLink, Modifier.aspectRatio(1f).padding(horizontal = DEFAULT_PADDING))
Row( Row(
horizontalArrangement = Arrangement.spacedBy(10.dp), horizontalArrangement = Arrangement.spacedBy(10.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(vertical = 10.dp) modifier = Modifier.padding(horizontal = DEFAULT_PADDING, vertical = 10.dp)
) { ) {
SimpleButton( SimpleButton(
stringResource(R.string.share_link), stringResource(R.string.share_link),
icon = Icons.Outlined.Share, icon = painterResource(R.drawable.ic_share),
click = share click = share
) )
SimpleButton( SimpleButton(
stringResource(R.string.delete_link), stringResource(R.string.delete_link),
icon = Icons.Outlined.Delete, icon = painterResource(R.drawable.ic_delete),
color = Color.Red, color = Color.Red,
click = deleteLink click = deleteLink
) )
} }
} }
} }
SectionBottomSpacer()
}
}
@Composable
private fun RoleSelectionRow(groupInfo: GroupInfo, selectedRole: MutableState<GroupMemberRole?>, enabled: Boolean = true) {
Row(
Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
val values = listOf(GroupMemberRole.Member, GroupMemberRole.Observer).map { it to it.text }
ExposedDropDownSettingRow(
generalGetString(R.string.initial_member_role),
values,
selectedRole,
icon = null,
enabled = rememberUpdatedState(enabled)
) { selectedRole.value = it }
} }
} }
@@ -126,7 +176,7 @@ fun ProgressIndicator() {
Modifier Modifier
.padding(horizontal = 2.dp) .padding(horizontal = 2.dp)
.size(30.dp), .size(30.dp),
color = HighOrLowlight, color = MaterialTheme.colors.secondary,
strokeWidth = 2.5.dp strokeWidth = 2.5.dp
) )
} }
@@ -1,29 +1,25 @@
package chat.simplex.app.views.chat.group package chat.simplex.app.views.chat.group
import InfoRow import InfoRow
import SectionDivider import SectionBottomSpacer
import SectionItemView import SectionDividerSpaced
import SectionSpacer import SectionSpacer
import SectionView import SectionView
import android.util.Log
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.compose.foundation.* import androidx.compose.foundation.*
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.material.* import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import chat.simplex.app.R import chat.simplex.app.R
import chat.simplex.app.TAG
import chat.simplex.app.model.* import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.* import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.chat.* import chat.simplex.app.views.chat.*
@@ -54,23 +50,16 @@ fun GroupMemberInfoView(
developerTools, developerTools,
connectionCode, connectionCode,
getContactChat = { chatModel.getContactChat(it) }, getContactChat = { chatModel.getContactChat(it) },
knownDirectChat = { openDirectChat = {
withApi {
chatModel.chatItems.clear()
chatModel.chatItems.addAll(it.chatItems)
chatModel.chatId.value = it.chatInfo.id
closeAll()
}
},
newDirectChat = {
withApi { withApi {
val c = chatModel.controller.apiGetChat(ChatType.Direct, it) val c = chatModel.controller.apiGetChat(ChatType.Direct, it)
if (c != null) { if (c != null) {
// TODO it's not correct to blindly set network status to connected - we should manage network status in model / backend if (chatModel.getContactChat(it) == null) {
val newChat = c.copy(serverInfo = Chat.ServerInfo(networkStatus = Chat.NetworkStatus.Connected())) chatModel.addChat(c)
chatModel.addChat(newChat) }
chatModel.chatItems.clear() chatModel.chatItems.clear()
chatModel.chatId.value = newChat.id chatModel.chatItems.addAll(c.chatItems)
chatModel.chatId.value = c.id
closeAll() closeAll()
} }
} }
@@ -127,7 +116,7 @@ fun GroupMemberInfoView(
} }
fun removeMemberDialog(groupInfo: GroupInfo, member: GroupMember, chatModel: ChatModel, close: (() -> Unit)? = null) { fun removeMemberDialog(groupInfo: GroupInfo, member: GroupMember, chatModel: ChatModel, close: (() -> Unit)? = null) {
AlertManager.shared.showAlertMsg( AlertManager.shared.showAlertDialog(
title = generalGetString(R.string.button_remove_member), title = generalGetString(R.string.button_remove_member),
text = generalGetString(R.string.member_will_be_removed_from_group_cannot_be_undone), text = generalGetString(R.string.member_will_be_removed_from_group_cannot_be_undone),
confirmText = generalGetString(R.string.remove_member_confirmation), confirmText = generalGetString(R.string.remove_member_confirmation),
@@ -139,7 +128,8 @@ fun removeMemberDialog(groupInfo: GroupInfo, member: GroupMember, chatModel: Cha
} }
close?.invoke() close?.invoke()
} }
} },
destructive = true,
) )
} }
@@ -152,8 +142,7 @@ fun GroupMemberInfoLayout(
developerTools: Boolean, developerTools: Boolean,
connectionCode: String?, connectionCode: String?,
getContactChat: (Long) -> Chat?, getContactChat: (Long) -> Chat?,
knownDirectChat: (Chat) -> Unit, openDirectChat: (Long) -> Unit,
newDirectChat: (Long) -> Unit,
removeMember: () -> Unit, removeMember: () -> Unit,
onRoleSelected: (GroupMemberRole) -> Unit, onRoleSelected: (GroupMemberRole) -> Unit,
switchMemberAddress: () -> Unit, switchMemberAddress: () -> Unit,
@@ -163,7 +152,6 @@ fun GroupMemberInfoLayout(
Modifier Modifier
.fillMaxWidth() .fillMaxWidth()
.verticalScroll(rememberScrollState()), .verticalScroll(rememberScrollState()),
horizontalAlignment = Alignment.Start
) { ) {
Row( Row(
Modifier.fillMaxWidth(), Modifier.fillMaxWidth(),
@@ -178,16 +166,8 @@ fun GroupMemberInfoLayout(
if (contactId != null) { if (contactId != null) {
SectionView { SectionView {
val chat = getContactChat(contactId) val chat = getContactChat(contactId)
if (chat != null && chat.chatInfo is ChatInfo.Direct && chat.chatInfo.contact.directOrUsed) { if ((chat != null && chat.chatInfo is ChatInfo.Direct && chat.chatInfo.contact.directOrUsed) || groupInfo.fullGroupPreferences.directMessages.on) {
OpenChatButton(onClick = { knownDirectChat(chat) }) OpenChatButton(onClick = { openDirectChat(contactId) })
if (connectionCode != null) {
SectionDivider()
}
} else if (groupInfo.fullGroupPreferences.directMessages.on) {
OpenChatButton(onClick = { newDirectChat(contactId) })
if (connectionCode != null) {
SectionDivider()
}
} }
if (connectionCode != null) { if (connectionCode != null) {
VerifyCodeButton(member.verified, verifyClicked) VerifyCodeButton(member.verified, verifyClicked)
@@ -199,36 +179,30 @@ fun GroupMemberInfoLayout(
SectionView(title = stringResource(R.string.member_info_section_title_member)) { SectionView(title = stringResource(R.string.member_info_section_title_member)) {
InfoRow(stringResource(R.string.info_row_group), groupInfo.displayName) InfoRow(stringResource(R.string.info_row_group), groupInfo.displayName)
SectionDivider()
val roles = remember { member.canChangeRoleTo(groupInfo) } val roles = remember { member.canChangeRoleTo(groupInfo) }
if (roles != null) { if (roles != null) {
SectionItemView { RoleSelectionRow(roles, newRole, onRoleSelected)
RoleSelectionRow(roles, newRole, onRoleSelected)
}
} else { } else {
InfoRow(stringResource(R.string.role_in_group), member.memberRole.text) InfoRow(stringResource(R.string.role_in_group), member.memberRole.text)
} }
val conn = member.activeConn val conn = member.activeConn
if (conn != null) { if (conn != null) {
SectionDivider()
val connLevelDesc = val connLevelDesc =
if (conn.connLevel == 0) stringResource(R.string.conn_level_desc_direct) if (conn.connLevel == 0) stringResource(R.string.conn_level_desc_direct)
else String.format(generalGetString(R.string.conn_level_desc_indirect), conn.connLevel) else String.format(generalGetString(R.string.conn_level_desc_indirect), conn.connLevel)
InfoRow(stringResource(R.string.info_row_connection), connLevelDesc) InfoRow(stringResource(R.string.info_row_connection), connLevelDesc)
} }
} }
SectionSpacer()
if (connStats != null) { if (connStats != null) {
SectionDividerSpaced()
SectionView(title = stringResource(R.string.conn_stats_section_title_servers)) { SectionView(title = stringResource(R.string.conn_stats_section_title_servers)) {
SwitchAddressButton(switchMemberAddress) SwitchAddressButton(switchMemberAddress)
SectionDivider()
val rcvServers = connStats.rcvServers val rcvServers = connStats.rcvServers
val sndServers = connStats.sndServers val sndServers = connStats.sndServers
if ((rcvServers != null && rcvServers.isNotEmpty()) || (sndServers != null && sndServers.isNotEmpty())) { if ((rcvServers != null && rcvServers.isNotEmpty()) || (sndServers != null && sndServers.isNotEmpty())) {
if (rcvServers != null && rcvServers.isNotEmpty()) { if (rcvServers != null && rcvServers.isNotEmpty()) {
SimplexServers(stringResource(R.string.receiving_via), rcvServers) SimplexServers(stringResource(R.string.receiving_via), rcvServers)
if (sndServers != null && sndServers.isNotEmpty()) { if (sndServers != null && sndServers.isNotEmpty()) {
SectionDivider()
SimplexServers(stringResource(R.string.sending_via), sndServers) SimplexServers(stringResource(R.string.sending_via), sndServers)
} }
} else if (sndServers != null && sndServers.isNotEmpty()) { } else if (sndServers != null && sndServers.isNotEmpty()) {
@@ -236,24 +210,23 @@ fun GroupMemberInfoLayout(
} }
} }
} }
SectionSpacer()
} }
if (member.canBeRemoved(groupInfo)) { if (member.canBeRemoved(groupInfo)) {
SectionDividerSpaced(maxBottomPadding = false)
SectionView { SectionView {
RemoveMemberButton(removeMember) RemoveMemberButton(removeMember)
} }
SectionSpacer()
} }
if (developerTools) { if (developerTools) {
SectionDividerSpaced()
SectionView(title = stringResource(R.string.section_title_for_console)) { SectionView(title = stringResource(R.string.section_title_for_console)) {
InfoRow(stringResource(R.string.info_row_local_name), member.localDisplayName) InfoRow(stringResource(R.string.info_row_local_name), member.localDisplayName)
SectionDivider()
InfoRow(stringResource(R.string.info_row_database_id), member.groupMemberId.toString()) InfoRow(stringResource(R.string.info_row_database_id), member.groupMemberId.toString())
} }
SectionSpacer()
} }
SectionBottomSpacer()
} }
} }
@@ -266,7 +239,7 @@ fun GroupMemberInfoHeader(member: GroupMember) {
ProfileImage(size = 192.dp, member.image, color = if (isInDarkTheme()) GroupDark else SettingsSecondaryLight) ProfileImage(size = 192.dp, member.image, color = if (isInDarkTheme()) GroupDark else SettingsSecondaryLight)
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
if (member.verified) { if (member.verified) {
Icon(Icons.Outlined.VerifiedUser, null, Modifier.padding(end = 6.dp, top = 4.dp).size(24.dp), tint = HighOrLowlight) Icon(painterResource(R.drawable.ic_verified_user), null, Modifier.padding(end = 6.dp, top = 4.dp).size(24.dp), tint = MaterialTheme.colors.secondary)
} }
Text( Text(
member.displayName, style = MaterialTheme.typography.h1.copy(fontWeight = FontWeight.Normal), member.displayName, style = MaterialTheme.typography.h1.copy(fontWeight = FontWeight.Normal),
@@ -289,7 +262,7 @@ fun GroupMemberInfoHeader(member: GroupMember) {
@Composable @Composable
fun RemoveMemberButton(onClick: () -> Unit) { fun RemoveMemberButton(onClick: () -> Unit) {
SettingsActionItem( SettingsActionItem(
Icons.Outlined.Delete, painterResource(R.drawable.ic_delete),
stringResource(R.string.button_remove_member), stringResource(R.string.button_remove_member),
click = onClick, click = onClick,
textColor = Color.Red, textColor = Color.Red,
@@ -300,7 +273,7 @@ fun RemoveMemberButton(onClick: () -> Unit) {
@Composable @Composable
fun OpenChatButton(onClick: () -> Unit) { fun OpenChatButton(onClick: () -> Unit) {
SettingsActionItem( SettingsActionItem(
Icons.Outlined.Message, painterResource(R.drawable.ic_chat),
stringResource(R.string.button_send_direct_message), stringResource(R.string.button_send_direct_message),
click = onClick, click = onClick,
textColor = MaterialTheme.colors.primary, textColor = MaterialTheme.colors.primary,
@@ -366,8 +339,7 @@ fun PreviewGroupMemberInfoLayout() {
developerTools = false, developerTools = false,
connectionCode = "123", connectionCode = "123",
getContactChat = { Chat.sampleData }, getContactChat = { Chat.sampleData },
knownDirectChat = {}, openDirectChat = {},
newDirectChat = {},
removeMember = {}, removeMember = {},
onRoleSelected = {}, onRoleSelected = {},
switchMemberAddress = {}, switchMemberAddress = {},
@@ -1,9 +1,9 @@
package chat.simplex.app.views.chat.group package chat.simplex.app.views.chat.group
import InfoRow import InfoRow
import SectionDivider import SectionBottomSpacer
import SectionDividerSpaced
import SectionItemView import SectionItemView
import SectionSpacer
import SectionTextFooter import SectionTextFooter
import SectionView import SectionView
import androidx.compose.foundation.* import androidx.compose.foundation.*
@@ -12,7 +12,6 @@ import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import chat.simplex.app.R import chat.simplex.app.R
@@ -45,7 +44,6 @@ fun GroupPreferencesView(m: ChatModel, chatId: String, close: () -> Unit,) {
if (preferences == currentPreferences) close() if (preferences == currentPreferences) close()
else showUnsavedChangesAlert({ savePrefs(close) }, close) else showUnsavedChangesAlert({ savePrefs(close) }, close)
}, },
background = if (isInDarkTheme()) MaterialTheme.colors.background else SettingsBackgroundLight
) { ) {
GroupPreferencesLayout( GroupPreferencesLayout(
preferences, preferences,
@@ -73,7 +71,6 @@ private fun GroupPreferencesLayout(
) { ) {
Column( Column(
Modifier.fillMaxWidth().verticalScroll(rememberScrollState()), Modifier.fillMaxWidth().verticalScroll(rememberScrollState()),
horizontalAlignment = Alignment.Start,
) { ) {
AppBarTitle(stringResource(R.string.group_preferences)) AppBarTitle(stringResource(R.string.group_preferences))
val timedMessages = remember(preferences) { mutableStateOf(preferences.timedMessages.enable) } val timedMessages = remember(preferences) { mutableStateOf(preferences.timedMessages.enable) }
@@ -87,29 +84,30 @@ private fun GroupPreferencesLayout(
applyPrefs(preferences.copy(timedMessages = TimedMessagesGroupPreference(enable = enable, ttl = currentPreferences.timedMessages.ttl))) applyPrefs(preferences.copy(timedMessages = TimedMessagesGroupPreference(enable = enable, ttl = currentPreferences.timedMessages.ttl)))
} }
} }
SectionSpacer() SectionDividerSpaced(true, maxBottomPadding = false)
val allowDirectMessages = remember(preferences) { mutableStateOf(preferences.directMessages.enable) } val allowDirectMessages = remember(preferences) { mutableStateOf(preferences.directMessages.enable) }
FeatureSection(GroupFeature.DirectMessages, allowDirectMessages, groupInfo, preferences, onTTLUpdated) { FeatureSection(GroupFeature.DirectMessages, allowDirectMessages, groupInfo, preferences, onTTLUpdated) {
applyPrefs(preferences.copy(directMessages = GroupPreference(enable = it))) applyPrefs(preferences.copy(directMessages = GroupPreference(enable = it)))
} }
SectionSpacer() SectionDividerSpaced(true, maxBottomPadding = false)
val allowFullDeletion = remember(preferences) { mutableStateOf(preferences.fullDelete.enable) } val allowFullDeletion = remember(preferences) { mutableStateOf(preferences.fullDelete.enable) }
FeatureSection(GroupFeature.FullDelete, allowFullDeletion, groupInfo, preferences, onTTLUpdated) { FeatureSection(GroupFeature.FullDelete, allowFullDeletion, groupInfo, preferences, onTTLUpdated) {
applyPrefs(preferences.copy(fullDelete = GroupPreference(enable = it))) applyPrefs(preferences.copy(fullDelete = GroupPreference(enable = it)))
} }
SectionSpacer() SectionDividerSpaced(true, maxBottomPadding = false)
val allowVoice = remember(preferences) { mutableStateOf(preferences.voice.enable) } val allowVoice = remember(preferences) { mutableStateOf(preferences.voice.enable) }
FeatureSection(GroupFeature.Voice, allowVoice, groupInfo, preferences, onTTLUpdated) { FeatureSection(GroupFeature.Voice, allowVoice, groupInfo, preferences, onTTLUpdated) {
applyPrefs(preferences.copy(voice = GroupPreference(enable = it))) applyPrefs(preferences.copy(voice = GroupPreference(enable = it)))
} }
if (groupInfo.canEdit) { if (groupInfo.canEdit) {
SectionSpacer() SectionDividerSpaced(maxTopPadding = true, maxBottomPadding = false)
ResetSaveButtons( ResetSaveButtons(
reset = reset, reset = reset,
save = savePrefs, save = savePrefs,
disabled = preferences == currentPreferences disabled = preferences == currentPreferences
) )
} }
SectionBottomSpacer()
} }
} }
@@ -124,22 +122,19 @@ private fun FeatureSection(
) { ) {
SectionView { SectionView {
val on = enableFeature.value == GroupFeatureEnabled.ON val on = enableFeature.value == GroupFeatureEnabled.ON
val icon = if (on) feature.iconFilled else feature.icon val icon = if (on) feature.iconFilled() else feature.icon
val iconTint = if (on) SimplexGreen else HighOrLowlight val iconTint = if (on) SimplexGreen else MaterialTheme.colors.secondary
val timedOn = feature == GroupFeature.TimedMessages && enableFeature.value == GroupFeatureEnabled.ON val timedOn = feature == GroupFeature.TimedMessages && enableFeature.value == GroupFeatureEnabled.ON
if (groupInfo.canEdit) { if (groupInfo.canEdit) {
SectionItemView { PreferenceToggleWithIcon(
PreferenceToggleWithIcon( feature.text,
feature.text, icon,
icon, iconTint,
iconTint, enableFeature.value == GroupFeatureEnabled.ON,
enableFeature.value == GroupFeatureEnabled.ON, ) { checked ->
) { checked -> onSelected(if (checked) GroupFeatureEnabled.ON else GroupFeatureEnabled.OFF)
onSelected(if (checked) GroupFeatureEnabled.ON else GroupFeatureEnabled.OFF)
}
} }
if (timedOn) { if (timedOn) {
SectionDivider()
val ttl = rememberSaveable(preferences.timedMessages) { mutableStateOf(preferences.timedMessages.ttl) } val ttl = rememberSaveable(preferences.timedMessages) { mutableStateOf(preferences.timedMessages.ttl) }
TimedMessagesTTLPicker(ttl, onTTLUpdated) TimedMessagesTTLPicker(ttl, onTTLUpdated)
} }
@@ -151,7 +146,6 @@ private fun FeatureSection(
iconTint = iconTint, iconTint = iconTint,
) )
if (timedOn) { if (timedOn) {
SectionDivider()
InfoRow(generalGetString(R.string.delete_after), TimedMessagesPreference.ttlText(preferences.timedMessages.ttl)) InfoRow(generalGetString(R.string.delete_after), TimedMessagesPreference.ttlText(preferences.timedMessages.ttl))
} }
} }
@@ -163,11 +157,10 @@ private fun FeatureSection(
private fun ResetSaveButtons(reset: () -> Unit, save: () -> Unit, disabled: Boolean) { private fun ResetSaveButtons(reset: () -> Unit, save: () -> Unit, disabled: Boolean) {
SectionView { SectionView {
SectionItemView(reset, disabled = disabled) { SectionItemView(reset, disabled = disabled) {
Text(stringResource(R.string.reset_verb), color = if (disabled) HighOrLowlight else MaterialTheme.colors.primary) Text(stringResource(R.string.reset_verb), color = if (disabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary)
} }
SectionDivider()
SectionItemView(save, disabled = disabled) { SectionItemView(save, disabled = disabled) {
Text(stringResource(R.string.save_and_notify_group_members), color = if (disabled) HighOrLowlight else MaterialTheme.colors.primary) Text(stringResource(R.string.save_and_notify_group_members), color = if (disabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary)
} }
} }
} }
@@ -1,7 +1,7 @@
package chat.simplex.app.views.chat.group package chat.simplex.app.views.chat.group
import SectionBottomSpacer
import android.content.res.Configuration import android.content.res.Configuration
import android.graphics.Bitmap
import android.net.Uri import android.net.Uri
import androidx.compose.foundation.* import androidx.compose.foundation.*
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
@@ -14,6 +14,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
@@ -23,6 +24,7 @@ import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.ProfileNameField import chat.simplex.app.views.ProfileNameField
import chat.simplex.app.views.helpers.* import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.isValidDisplayName import chat.simplex.app.views.isValidDisplayName
import chat.simplex.app.views.onboarding.ReadableText
import chat.simplex.app.views.usersettings.* import chat.simplex.app.views.usersettings.*
import com.google.accompanist.insets.ProvideWindowInsets import com.google.accompanist.insets.ProvideWindowInsets
import com.google.accompanist.insets.navigationBarsWithImePadding import com.google.accompanist.insets.navigationBarsWithImePadding
@@ -53,14 +55,32 @@ fun GroupProfileLayout(
saveProfile: (GroupProfile) -> Unit, saveProfile: (GroupProfile) -> Unit,
) { ) {
val bottomSheetModalState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden) val bottomSheetModalState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden)
val displayName = remember { mutableStateOf(groupProfile.displayName) } val displayName = rememberSaveable { mutableStateOf(groupProfile.displayName) }
val fullName = remember { mutableStateOf(groupProfile.fullName) } val fullName = rememberSaveable { mutableStateOf(groupProfile.fullName) }
val chosenImage = rememberSaveable { mutableStateOf<Uri?>(null) } val chosenImage = rememberSaveable { mutableStateOf<Uri?>(null) }
val profileImage = rememberSaveable { mutableStateOf(groupProfile.image) } val profileImage = rememberSaveable { mutableStateOf(groupProfile.image) }
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val scrollState = rememberScrollState() val scrollState = rememberScrollState()
val focusRequester = remember { FocusRequester() } val focusRequester = remember { FocusRequester() }
val dataUnchanged =
displayName.value == groupProfile.displayName &&
fullName.value == groupProfile.fullName &&
groupProfile.image == profileImage.value
val closeWithAlert = {
if (dataUnchanged || !(displayName.value.isNotEmpty() && isValidDisplayName(displayName.value))) {
close()
} else {
showUnsavedChangesAlert({
saveProfile(
groupProfile.copy(
displayName = displayName.value,
fullName = fullName.value,
image = profileImage.value
)
)
}, close)
}
}
ProvideWindowInsets(windowInsetsAnimationsEnabled = true) { ProvideWindowInsets(windowInsetsAnimationsEnabled = true) {
ModalBottomSheetLayout( ModalBottomSheetLayout(
scrimColor = Color.Black.copy(alpha = 0.12F), scrimColor = Color.Black.copy(alpha = 0.12F),
@@ -76,23 +96,16 @@ fun GroupProfileLayout(
sheetState = bottomSheetModalState, sheetState = bottomSheetModalState,
sheetShape = RoundedCornerShape(topStart = 18.dp, topEnd = 18.dp) sheetShape = RoundedCornerShape(topStart = 18.dp, topEnd = 18.dp)
) { ) {
ModalView(close = close) { ModalView(close = closeWithAlert) {
Column( Column(
Modifier Modifier
.verticalScroll(scrollState) .verticalScroll(scrollState)
.padding(horizontal = DEFAULT_PADDING),
horizontalAlignment = Alignment.Start
) { ) {
Text(
stringResource(R.string.group_profile_is_stored_on_members_devices),
Modifier.padding(bottom = 24.dp),
color = MaterialTheme.colors.onBackground,
lineHeight = 22.sp
)
Column( Column(
Modifier.fillMaxWidth(), Modifier.fillMaxWidth()
horizontalAlignment = Alignment.Start .padding(horizontal = DEFAULT_PADDING)
) { ) {
ReadableText(R.string.group_profile_is_stored_on_members_devices, TextAlign.Center)
Box( Box(
Modifier Modifier
.fillMaxWidth() .fillMaxWidth()
@@ -101,7 +114,7 @@ fun GroupProfileLayout(
) { ) {
Box(contentAlignment = Alignment.TopEnd) { Box(contentAlignment = Alignment.TopEnd) {
Box(contentAlignment = Alignment.Center) { Box(contentAlignment = Alignment.Center) {
ProfileImage(192.dp, profileImage.value) ProfileImage(108.dp, profileImage.value, color = MaterialTheme.colors.secondary.copy(alpha = 0.1f))
EditImageButton { scope.launch { bottomSheetModalState.show() } } EditImageButton { scope.launch { bottomSheetModalState.show() } }
} }
if (profileImage.value != null) { if (profileImage.value != null) {
@@ -109,51 +122,54 @@ fun GroupProfileLayout(
} }
} }
} }
Text( Row(Modifier.padding(bottom = DEFAULT_PADDING_HALF).fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
stringResource(R.string.group_display_name_field), Text(
Modifier.padding(bottom = 3.dp) stringResource(R.string.group_display_name_field),
) fontSize = 16.sp
ProfileNameField(displayName, focusRequester) )
val errorText = if (!isValidDisplayName(displayName.value)) stringResource(R.string.display_name_cannot_contain_whitespace) else "" if (!isValidDisplayName(displayName.value)) {
Text( Spacer(Modifier.size(DEFAULT_PADDING_HALF))
errorText,
fontSize = 15.sp,
color = MaterialTheme.colors.error
)
Spacer(Modifier.height(3.dp))
Text(
stringResource(R.string.group_full_name_field),
Modifier.padding(bottom = 5.dp)
)
ProfileNameField(fullName)
Spacer(Modifier.height(16.dp))
Row {
TextButton(stringResource(R.string.cancel_verb)) {
close.invoke()
}
Spacer(Modifier.padding(horizontal = 8.dp))
val enabled = displayName.value.isNotEmpty() && isValidDisplayName(displayName.value)
if (enabled) {
Text( Text(
stringResource(R.string.save_group_profile), stringResource(R.string.no_spaces),
modifier = Modifier.clickable { fontSize = 16.sp,
saveProfile(groupProfile.copy( color = Color.Red
displayName = displayName.value,
fullName = fullName.value,
image = profileImage.value
))
},
color = MaterialTheme.colors.primary
)
} else {
Text(
stringResource(R.string.save_group_profile),
color = HighOrLowlight
) )
} }
} }
ProfileNameField(displayName, "", ::isValidDisplayName, focusRequester)
Spacer(Modifier.height(DEFAULT_PADDING))
Text(
stringResource(R.string.group_full_name_field),
fontSize = 16.sp,
modifier = Modifier.padding(bottom = DEFAULT_PADDING_HALF)
)
ProfileNameField(fullName)
Spacer(Modifier.height(DEFAULT_PADDING))
val enabled = !dataUnchanged && displayName.value.isNotEmpty() && isValidDisplayName(displayName.value)
if (enabled) {
Text(
stringResource(R.string.save_group_profile),
modifier = Modifier.clickable {
saveProfile(
groupProfile.copy(
displayName = displayName.value,
fullName = fullName.value,
image = profileImage.value
)
)
},
color = MaterialTheme.colors.primary
)
} else {
Text(
stringResource(R.string.save_group_profile),
color = MaterialTheme.colors.secondary
)
}
} }
SectionBottomSpacer()
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
delay(300) delay(300)
focusRequester.requestFocus() focusRequester.requestFocus()
@@ -164,6 +180,16 @@ fun GroupProfileLayout(
} }
} }
private fun showUnsavedChangesAlert(save: () -> Unit, revert: () -> Unit) {
AlertManager.shared.showAlertDialogStacked(
title = generalGetString(R.string.save_preferences_question),
confirmText = generalGetString(R.string.save_and_notify_group_members),
dismissText = generalGetString(R.string.exit_without_saving),
onConfirm = save,
onDismiss = revert,
)
}
@Preview(showBackground = true) @Preview(showBackground = true)
@Preview( @Preview(
uiMode = Configuration.UI_MODE_NIGHT_YES, uiMode = Configuration.UI_MODE_NIGHT_YES,
@@ -0,0 +1,95 @@
package chat.simplex.app.views.chat.group
import SectionBottomSpacer
import SectionItemView
import SectionSpacer
import SectionView
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import chat.simplex.app.R
import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.helpers.*
@Composable
fun GroupWelcomeView(m: ChatModel, groupInfo: GroupInfo, close: () -> Unit) {
var groupInfo by remember { mutableStateOf(groupInfo) }
val welcomeText = remember { mutableStateOf(groupInfo.groupProfile.description ?: "") }
fun save(afterSave: () -> Unit = {}) {
withApi {
var welcome: String? = welcomeText.value.trim('\n', ' ')
if (welcome?.length == 0) {
welcome = null
}
val groupProfileUpdated = groupInfo.groupProfile.copy(description = welcome)
val res = m.controller.apiUpdateGroup(groupInfo.groupId, groupProfileUpdated)
if (res != null) {
groupInfo = res
m.updateGroup(res)
welcomeText.value = welcome ?: ""
}
afterSave()
}
}
ModalView(
close = {
if (welcomeText.value == groupInfo.groupProfile.description || (welcomeText.value == "" && groupInfo.groupProfile.description == null)) close()
else showUnsavedChangesAlert({ save(close) }, close)
},
) {
GroupWelcomeLayout(
welcomeText,
groupInfo,
save = ::save
)
}
}
@Composable
private fun GroupWelcomeLayout(
welcomeText: MutableState<String>,
groupInfo: GroupInfo,
save: () -> Unit,
) {
Column(
Modifier.fillMaxWidth().verticalScroll(rememberScrollState()),
) {
AppBarTitle(stringResource(R.string.group_welcome_title))
val welcomeText = remember { welcomeText }
TextEditor(Modifier.padding(horizontal = DEFAULT_PADDING).height(160.dp), text = welcomeText)
SectionSpacer()
SaveButton(
save = save,
disabled = welcomeText.value == groupInfo.groupProfile.description || (welcomeText.value == "" && groupInfo.groupProfile.description == null)
)
SectionBottomSpacer()
}
}
@Composable
private fun SaveButton(save: () -> Unit, disabled: Boolean) {
SectionView {
SectionItemView(save, disabled = disabled) {
Text(stringResource(R.string.save_and_update_group_profile), color = if (disabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary)
}
}
}
private fun showUnsavedChangesAlert(save: () -> Unit, revert: () -> Unit) {
AlertManager.shared.showAlertDialogStacked(
title = generalGetString(R.string.save_welcome_message_question),
confirmText = generalGetString(R.string.save_and_update_group_profile),
dismissText = generalGetString(R.string.exit_without_saving),
onConfirm = save,
onDismiss = revert,
)
}
@@ -1,15 +1,12 @@
package chat.simplex.app.views.chat.item package chat.simplex.app.views.chat.item
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.material.Icon import androidx.compose.material.*
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.PhoneInTalk
import androidx.compose.material.icons.outlined.*
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
@@ -24,28 +21,28 @@ fun CICallItemView(cInfo: ChatInfo, cItem: ChatItem, status: CICallStatus, durat
Modifier Modifier
.padding(horizontal = 4.dp) .padding(horizontal = 4.dp)
.padding(bottom = 8.dp), horizontalAlignment = Alignment.CenterHorizontally) { .padding(bottom = 8.dp), horizontalAlignment = Alignment.CenterHorizontally) {
@Composable fun ConnectingCallIcon() = Icon(Icons.Outlined.SettingsPhone, stringResource(R.string.icon_descr_call_connecting), tint = SimplexGreen) @Composable fun ConnectingCallIcon() = Icon(painterResource(R.drawable.ic_settings_phone), stringResource(R.string.icon_descr_call_connecting), tint = SimplexGreen)
when (status) { when (status) {
CICallStatus.Pending -> if (sent) { CICallStatus.Pending -> if (sent) {
Icon(Icons.Outlined.Call, stringResource(R.string.icon_descr_call_pending_sent)) Icon(painterResource(R.drawable.ic_call), stringResource(R.string.icon_descr_call_pending_sent))
} else { } else {
AcceptCallButton(cInfo, acceptCall) AcceptCallButton(cInfo, acceptCall)
} }
CICallStatus.Missed -> Icon(Icons.Outlined.Call, stringResource(R.string.icon_descr_call_missed), tint = Color.Red) CICallStatus.Missed -> Icon(painterResource(R.drawable.ic_call), stringResource(R.string.icon_descr_call_missed), tint = Color.Red)
CICallStatus.Rejected -> Icon(Icons.Outlined.CallEnd, stringResource(R.string.icon_descr_call_rejected), tint = Color.Red) CICallStatus.Rejected -> Icon(painterResource(R.drawable.ic_call_end), stringResource(R.string.icon_descr_call_rejected), tint = Color.Red)
CICallStatus.Accepted -> ConnectingCallIcon() CICallStatus.Accepted -> ConnectingCallIcon()
CICallStatus.Negotiated -> ConnectingCallIcon() CICallStatus.Negotiated -> ConnectingCallIcon()
CICallStatus.Progress -> Icon(Icons.Filled.PhoneInTalk, stringResource(R.string.icon_descr_call_progress), tint = SimplexGreen) CICallStatus.Progress -> Icon(painterResource(R.drawable.ic_phone_in_talk_filled), stringResource(R.string.icon_descr_call_progress), tint = SimplexGreen)
CICallStatus.Ended -> Row { CICallStatus.Ended -> Row {
Icon(Icons.Outlined.CallEnd, stringResource(R.string.icon_descr_call_ended), tint = HighOrLowlight, modifier = Modifier.padding(end = 4.dp)) Icon(painterResource(R.drawable.ic_call_end), stringResource(R.string.icon_descr_call_ended), tint = MaterialTheme.colors.secondary, modifier = Modifier.padding(end = 4.dp))
Text(durationText(duration), color = HighOrLowlight) Text(durationText(duration), color = MaterialTheme.colors.secondary)
} }
CICallStatus.Error -> {} CICallStatus.Error -> {}
} }
Text( Text(
cItem.timestampText, cItem.timestampText,
color = HighOrLowlight, color = MaterialTheme.colors.secondary,
fontSize = 14.sp, fontSize = 14.sp,
modifier = Modifier.padding(start = 3.dp) modifier = Modifier.padding(start = 3.dp)
) )
@@ -55,9 +52,9 @@ fun CICallItemView(cInfo: ChatInfo, cItem: ChatItem, status: CICallStatus, durat
@Composable @Composable
fun AcceptCallButton(cInfo: ChatInfo, acceptCall: (Contact) -> Unit) { fun AcceptCallButton(cInfo: ChatInfo, acceptCall: (Contact) -> Unit) {
if (cInfo is ChatInfo.Direct) { if (cInfo is ChatInfo.Direct) {
SimpleButton(stringResource(R.string.answer_call), Icons.Outlined.RingVolume) { acceptCall(cInfo.contact) } SimpleButton(stringResource(R.string.answer_call), painterResource(R.drawable.ic_ring_volume)) { acceptCall(cInfo.contact) }
} else { } else {
Icon(Icons.Outlined.RingVolume, stringResource(R.string.answer_call), tint = HighOrLowlight) Icon(painterResource(R.drawable.ic_ring_volume), stringResource(R.string.answer_call), tint = MaterialTheme.colors.secondary)
} }
// if case let .direct(contact) = chatInfo { // if case let .direct(contact) = chatInfo {
// Button { // Button {
@@ -154,4 +151,4 @@ fun AcceptCallButton(cInfo: ChatInfo, acceptCall: (Contact) -> Unit) {
// Image(systemName: "phone.arrow.down.left").foregroundColor(.secondary) // Image(systemName: "phone.arrow.down.left").foregroundColor(.secondary)
// } // }
// } // }
//} //}
@@ -6,7 +6,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import chat.simplex.app.model.* import chat.simplex.app.model.*
@@ -16,14 +16,14 @@ fun CIChatFeatureView(
chatItem: ChatItem, chatItem: ChatItem,
feature: Feature, feature: Feature,
iconColor: Color, iconColor: Color,
icon: ImageVector? = null icon: Painter? = null
) { ) {
Row( Row(
Modifier.padding(horizontal = 6.dp, vertical = 6.dp), Modifier.padding(horizontal = 6.dp, vertical = 6.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp) horizontalArrangement = Arrangement.spacedBy(4.dp)
) { ) {
Icon(icon ?: feature.iconFilled, feature.text, Modifier.size(18.dp), tint = iconColor) Icon(icon ?: feature.iconFilled(), feature.text, Modifier.size(18.dp), tint = iconColor)
Text( Text(
chatEventText(chatItem), chatEventText(chatItem),
Modifier, Modifier,
@@ -13,8 +13,7 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import chat.simplex.app.model.ChatItem import chat.simplex.app.model.ChatItem
import chat.simplex.app.ui.theme.HighOrLowlight import chat.simplex.app.ui.theme.*
import chat.simplex.app.ui.theme.SimpleXTheme
@Composable @Composable
fun CIEventView(ci: ChatItem) { fun CIEventView(ci: ChatItem) {
@@ -22,28 +21,25 @@ fun CIEventView(ci: ChatItem) {
fun chatEventTextView(text: AnnotatedString) { fun chatEventTextView(text: AnnotatedString) {
Text(text, style = MaterialTheme.typography.body1.copy(lineHeight = 22.sp)) Text(text, style = MaterialTheme.typography.body1.copy(lineHeight = 22.sp))
} }
Row(
Surface { Modifier.padding(horizontal = 6.dp, vertical = 6.dp),
Row( verticalAlignment = Alignment.CenterVertically
Modifier.padding(horizontal = 6.dp, vertical = 6.dp), ) {
verticalAlignment = Alignment.CenterVertically val memberDisplayName = ci.memberDisplayName
) { if (memberDisplayName != null) {
val memberDisplayName = ci.memberDisplayName chatEventTextView(
if (memberDisplayName != null) { buildAnnotatedString {
chatEventTextView( withStyle(chatEventStyle) { append(memberDisplayName) }
buildAnnotatedString { append(" ")
withStyle(chatEventStyle) { append(memberDisplayName) } }.plus(chatEventText(ci))
append(" ") )
}.plus(chatEventText(ci)) } else {
) chatEventTextView(chatEventText(ci))
} else {
chatEventTextView(chatEventText(ci))
}
} }
} }
} }
val chatEventStyle = SpanStyle(fontSize = 12.sp, fontWeight = FontWeight.Light, color = HighOrLowlight) val chatEventStyle = SpanStyle(fontSize = 12.sp, fontWeight = FontWeight.Light, color = CurrentColors.value.colors.secondary)
fun chatEventText(ci: ChatItem): AnnotatedString = fun chatEventText(ci: ChatItem): AnnotatedString =
buildAnnotatedString { buildAnnotatedString {
@@ -11,7 +11,6 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import chat.simplex.app.R import chat.simplex.app.R
import chat.simplex.app.model.* import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.HighOrLowlight
import chat.simplex.app.views.helpers.generalGetString import chat.simplex.app.views.helpers.generalGetString
@Composable @Composable
@@ -27,7 +26,7 @@ fun CIFeaturePreferenceView(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp) horizontalArrangement = Arrangement.spacedBy(4.dp)
) { ) {
Icon(feature.icon, feature.text, Modifier.size(18.dp), tint = HighOrLowlight) Icon(feature.icon, feature.text, Modifier.size(18.dp), tint = MaterialTheme.colors.secondary)
if (contact != null && allowed != FeatureAllowed.NO && contact.allowsFeature(feature) && !contact.userAllowsFeature(feature)) { if (contact != null && allowed != FeatureAllowed.NO && contact.allowsFeature(feature) && !contact.userAllowsFeature(feature)) {
val acceptStyle = SpanStyle(color = MaterialTheme.colors.primary, fontSize = 12.sp) val acceptStyle = SpanStyle(color = MaterialTheme.colors.primary, fontSize = 12.sp)
val setParam = feature == ChatFeature.TimedMessages && contact.mergedPreferences.timedMessages.userPreference.pref.ttl == null val setParam = feature == ChatFeature.TimedMessages && contact.mergedPreferences.timedMessages.userPreference.pref.ttl == null
@@ -48,7 +47,7 @@ fun CIFeaturePreferenceView(
) )
} else { } else {
Text(chatItem.content.text + " " + chatItem.timestampText, Text(chatItem.content.text + " " + chatItem.timestampText,
fontSize = 12.sp, fontWeight = FontWeight.Light, color = HighOrLowlight) fontSize = 12.sp, fontWeight = FontWeight.Light, color = MaterialTheme.colors.secondary)
} }
} }
} }
@@ -3,19 +3,18 @@ package chat.simplex.app.views.chat.item
import android.widget.Toast import android.widget.Toast
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CornerSize
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.* import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.InsertDriveFile
import androidx.compose.material.icons.outlined.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.* import androidx.compose.ui.tooling.preview.*
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@@ -37,14 +36,14 @@ fun CIFileView(
@Composable @Composable
fun fileIcon( fun fileIcon(
innerIcon: ImageVector? = null, innerIcon: Painter? = null,
color: Color = if (isInDarkTheme()) FileDark else FileLight color: Color = if (isInDarkTheme()) FileDark else FileLight
) { ) {
Box( Box(
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
Icon( Icon(
Icons.Filled.InsertDriveFile, painterResource(R.drawable.ic_draft_filled),
stringResource(R.string.icon_descr_file), stringResource(R.string.icon_descr_file),
Modifier.fillMaxSize(), Modifier.fillMaxSize(),
tint = color tint = color
@@ -64,7 +63,7 @@ fun CIFileView(
fun fileSizeValid(): Boolean { fun fileSizeValid(): Boolean {
if (file != null) { if (file != null) {
return file.fileSize <= MAX_FILE_SIZE return file.fileSize <= getMaxFileSize(file.fileProtocol)
} }
return false return false
} }
@@ -72,22 +71,30 @@ fun CIFileView(
fun fileAction() { fun fileAction() {
if (file != null) { if (file != null) {
when (file.fileStatus) { when (file.fileStatus) {
CIFileStatus.RcvInvitation -> { is CIFileStatus.RcvInvitation -> {
if (fileSizeValid()) { if (fileSizeValid()) {
receiveFile(file.fileId) receiveFile(file.fileId)
} else { } else {
AlertManager.shared.showAlertMsg( AlertManager.shared.showAlertMsg(
generalGetString(R.string.large_file), generalGetString(R.string.large_file),
String.format(generalGetString(R.string.contact_sent_large_file), formatBytes(MAX_FILE_SIZE)) String.format(generalGetString(R.string.contact_sent_large_file), formatBytes(getMaxFileSize(file.fileProtocol)))
) )
} }
} }
CIFileStatus.RcvAccepted -> is CIFileStatus.RcvAccepted ->
AlertManager.shared.showAlertMsg( when (file.fileProtocol) {
generalGetString(R.string.waiting_for_file), FileProtocol.XFTP ->
String.format(generalGetString(R.string.file_will_be_received_when_contact_is_online), MAX_FILE_SIZE) AlertManager.shared.showAlertMsg(
) generalGetString(R.string.waiting_for_file),
CIFileStatus.RcvComplete -> { generalGetString(R.string.file_will_be_received_when_contact_completes_uploading)
)
FileProtocol.SMP ->
AlertManager.shared.showAlertMsg(
generalGetString(R.string.waiting_for_file),
generalGetString(R.string.file_will_be_received_when_contact_is_online)
)
}
is CIFileStatus.RcvComplete -> {
val filePath = getLoadedFilePath(context, file) val filePath = getLoadedFilePath(context, file)
if (filePath != null) { if (filePath != null) {
saveFileLauncher.launch(file.fileName) saveFileLauncher.launch(file.fileName)
@@ -105,10 +112,24 @@ fun CIFileView(
CircularProgressIndicator( CircularProgressIndicator(
Modifier.size(32.dp), Modifier.size(32.dp),
color = if (isInDarkTheme()) FileDark else FileLight, color = if (isInDarkTheme()) FileDark else FileLight,
strokeWidth = 4.dp strokeWidth = 3.dp
) )
} }
@Composable
fun progressCircle(progress: Long, total: Long) {
val angle = 360f * (progress.toDouble() / total.toDouble()).toFloat()
val strokeWidth = with(LocalDensity.current) { 3.dp.toPx() }
val strokeColor = if (isInDarkTheme()) FileDark else FileLight
Surface(
Modifier.drawRingModifier(angle, strokeColor, strokeWidth),
color = Color.Transparent,
shape = MaterialTheme.shapes.small.copy(CornerSize(percent = 50))
) {
Box(Modifier.size(32.dp))
}
}
@Composable @Composable
fun fileIndicator() { fun fileIndicator() {
Box( Box(
@@ -120,19 +141,34 @@ fun CIFileView(
) { ) {
if (file != null) { if (file != null) {
when (file.fileStatus) { when (file.fileStatus) {
CIFileStatus.SndStored -> fileIcon() is CIFileStatus.SndStored ->
CIFileStatus.SndTransfer -> progressIndicator() when (file.fileProtocol) {
CIFileStatus.SndComplete -> fileIcon(innerIcon = Icons.Filled.Check) FileProtocol.XFTP -> progressIndicator()
CIFileStatus.SndCancelled -> fileIcon(innerIcon = Icons.Outlined.Close) FileProtocol.SMP -> fileIcon()
CIFileStatus.RcvInvitation -> }
is CIFileStatus.SndTransfer ->
when (file.fileProtocol) {
FileProtocol.XFTP -> progressCircle(file.fileStatus.sndProgress, file.fileStatus.sndTotal)
FileProtocol.SMP -> progressIndicator()
}
is CIFileStatus.SndComplete -> fileIcon(innerIcon = painterResource(R.drawable.ic_check_filled))
is CIFileStatus.SndCancelled -> fileIcon(innerIcon = painterResource(R.drawable.ic_close))
is CIFileStatus.SndError -> fileIcon(innerIcon = painterResource(R.drawable.ic_close))
is CIFileStatus.RcvInvitation ->
if (fileSizeValid()) if (fileSizeValid())
fileIcon(innerIcon = Icons.Outlined.ArrowDownward, color = MaterialTheme.colors.primary) fileIcon(innerIcon = painterResource(R.drawable.ic_arrow_downward), color = MaterialTheme.colors.primary)
else else
fileIcon(innerIcon = Icons.Outlined.PriorityHigh, color = WarningOrange) fileIcon(innerIcon = painterResource(R.drawable.ic_priority_high), color = WarningOrange)
CIFileStatus.RcvAccepted -> fileIcon(innerIcon = Icons.Outlined.MoreHoriz) is CIFileStatus.RcvAccepted -> fileIcon(innerIcon = painterResource(R.drawable.ic_more_horiz))
CIFileStatus.RcvTransfer -> progressIndicator() is CIFileStatus.RcvTransfer ->
CIFileStatus.RcvComplete -> fileIcon() if (file.fileProtocol == FileProtocol.XFTP && file.fileStatus.rcvProgress < file.fileStatus.rcvTotal) {
CIFileStatus.RcvCancelled -> fileIcon(innerIcon = Icons.Outlined.Close) progressCircle(file.fileStatus.rcvProgress, file.fileStatus.rcvTotal)
} else {
progressIndicator()
}
is CIFileStatus.RcvComplete -> fileIcon()
is CIFileStatus.RcvCancelled -> fileIcon(innerIcon = painterResource(R.drawable.ic_close))
is CIFileStatus.RcvError -> fileIcon(innerIcon = painterResource(R.drawable.ic_close))
} }
} else { } else {
fileIcon() fileIcon()
@@ -151,16 +187,14 @@ fun CIFileView(
else else
" " " "
if (file != null) { if (file != null) {
Column( Column {
horizontalAlignment = Alignment.Start
) {
Text( Text(
file.fileName, file.fileName,
maxLines = 1 maxLines = 1
) )
Text( Text(
formatBytes(file.fileSize) + metaReserve, formatBytes(file.fileSize) + metaReserve,
color = HighOrLowlight, color = MaterialTheme.colors.secondary,
fontSize = 14.sp, fontSize = 14.sp,
maxLines = 1 maxLines = 1
) )
@@ -174,14 +208,14 @@ fun CIFileView(
class ChatItemProvider: PreviewParameterProvider<ChatItem> { class ChatItemProvider: PreviewParameterProvider<ChatItem> {
private val sentFile = ChatItem( private val sentFile = ChatItem(
chatDir = CIDirection.DirectSnd(), chatDir = CIDirection.DirectSnd(),
meta = CIMeta.getSample(1, Clock.System.now(), "", CIStatus.SndSent(), itemDeleted = false, itemEdited = true, editable = false), meta = CIMeta.getSample(1, Clock.System.now(), "", CIStatus.SndSent(), itemEdited = true),
content = CIContent.SndMsgContent(msgContent = MsgContent.MCFile("")), content = CIContent.SndMsgContent(msgContent = MsgContent.MCFile("")),
quotedItem = null, quotedItem = null,
file = CIFile.getSample(fileStatus = CIFileStatus.SndComplete) file = CIFile.getSample(fileStatus = CIFileStatus.SndComplete)
) )
private val fileChatItemWtFile = ChatItem( private val fileChatItemWtFile = ChatItem(
chatDir = CIDirection.DirectRcv(), chatDir = CIDirection.DirectRcv(),
meta = CIMeta.getSample(1, Clock.System.now(), "", CIStatus.RcvRead(), itemDeleted = false, itemEdited = false, editable = false), meta = CIMeta.getSample(1, Clock.System.now(), "", CIStatus.RcvRead(), ),
content = CIContent.RcvMsgContent(msgContent = MsgContent.MCFile("")), content = CIContent.RcvMsgContent(msgContent = MsgContent.MCFile("")),
quotedItem = null, quotedItem = null,
file = null file = null
@@ -191,7 +225,7 @@ class ChatItemProvider: PreviewParameterProvider<ChatItem> {
ChatItem.getFileMsgContentSample(), ChatItem.getFileMsgContentSample(),
ChatItem.getFileMsgContentSample(fileName = "some_long_file_name_here", fileStatus = CIFileStatus.RcvInvitation), ChatItem.getFileMsgContentSample(fileName = "some_long_file_name_here", fileStatus = CIFileStatus.RcvInvitation),
ChatItem.getFileMsgContentSample(fileStatus = CIFileStatus.RcvAccepted), ChatItem.getFileMsgContentSample(fileStatus = CIFileStatus.RcvAccepted),
ChatItem.getFileMsgContentSample(fileStatus = CIFileStatus.RcvTransfer), ChatItem.getFileMsgContentSample(fileStatus = CIFileStatus.RcvTransfer(rcvProgress = 7, rcvTotal = 10)),
ChatItem.getFileMsgContentSample(fileStatus = CIFileStatus.RcvCancelled), ChatItem.getFileMsgContentSample(fileStatus = CIFileStatus.RcvCancelled),
ChatItem.getFileMsgContentSample(fileSize = 1_000_000_000, fileStatus = CIFileStatus.RcvInvitation), ChatItem.getFileMsgContentSample(fileSize = 1_000_000_000, fileStatus = CIFileStatus.RcvInvitation),
ChatItem.getFileMsgContentSample(text = "Hello there", fileStatus = CIFileStatus.RcvInvitation), ChatItem.getFileMsgContentSample(text = "Hello there", fileStatus = CIFileStatus.RcvInvitation),
@@ -1,13 +1,10 @@
package chat.simplex.app.views.chat.item package chat.simplex.app.views.chat.item
import android.content.res.Configuration import android.content.res.Configuration
import android.util.Log
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.* import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.SupervisedUserCircle
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@@ -46,7 +43,7 @@ fun CIGroupInvitationView(
.padding(vertical = 4.dp) .padding(vertical = 4.dp)
.padding(end = 2.dp) .padding(end = 2.dp)
) { ) {
ProfileImage(size = 60.dp, image = groupInvitation.groupProfile.image, icon = Icons.Filled.SupervisedUserCircle, color = iconColor) ProfileImage(size = 60.dp, image = groupInvitation.groupProfile.image, icon = R.drawable.ic_supervised_user_circle_filled, color = iconColor)
Spacer(Modifier.padding(horizontal = 3.dp)) Spacer(Modifier.padding(horizontal = 3.dp))
Column( Column(
Modifier.defaultMinSize(minHeight = 60.dp), Modifier.defaultMinSize(minHeight = 60.dp),
@@ -71,12 +68,14 @@ fun CIGroupInvitationView(
} }
} }
val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage
val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage
Surface( Surface(
modifier = if (action) Modifier.clickable(onClick = { modifier = if (action) Modifier.clickable(onClick = {
joinGroup(groupInvitation.groupId) joinGroup(groupInvitation.groupId)
}) else Modifier, }) else Modifier,
shape = RoundedCornerShape(18.dp), shape = RoundedCornerShape(18.dp),
color = if (sent) SentColorLight else ReceivedColorLight, color = if (sent) sentColor else receivedColor,
) { ) {
Box( Box(
Modifier Modifier
@@ -89,7 +88,6 @@ fun CIGroupInvitationView(
Modifier Modifier
.defaultMinSize(minWidth = 220.dp) .defaultMinSize(minWidth = 220.dp)
.padding(bottom = 4.dp), .padding(bottom = 4.dp),
horizontalAlignment = Alignment.Start
) { ) {
groupInfoView() groupInfoView()
Column(Modifier.padding(top = 2.dp, start = 5.dp)) { Column(Modifier.padding(top = 2.dp, start = 5.dp)) {
@@ -108,7 +106,7 @@ fun CIGroupInvitationView(
} }
Text( Text(
ci.timestampText, ci.timestampText,
color = HighOrLowlight, color = MaterialTheme.colors.secondary,
fontSize = 14.sp, fontSize = 14.sp,
modifier = Modifier.padding(start = 3.dp) modifier = Modifier.padding(start = 3.dp)
) )
@@ -2,15 +2,12 @@ package chat.simplex.app.views.chat.item
import android.graphics.Bitmap import android.graphics.Bitmap
import android.os.Build.VERSION.SDK_INT import android.os.Build.VERSION.SDK_INT
import androidx.annotation.StringRes
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.material.CircularProgressIndicator import androidx.compose.material.CircularProgressIndicator
import androidx.compose.material.Icon import androidx.compose.material.Icon
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.outlined.ArrowDownward
import androidx.compose.material.icons.outlined.MoreHoriz
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@@ -21,14 +18,14 @@ import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.layoutId import androidx.compose.ui.layout.layoutId
import androidx.compose.ui.platform.* import androidx.compose.ui.platform.*
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import chat.simplex.app.* import chat.simplex.app.*
import chat.simplex.app.R import chat.simplex.app.R
import chat.simplex.app.model.CIFile import chat.simplex.app.model.*
import chat.simplex.app.model.CIFileStatus
import chat.simplex.app.views.helpers.* import chat.simplex.app.views.helpers.*
import coil.ImageLoader import coil.ImageLoader
import coil.compose.rememberAsyncImagePainter import coil.compose.rememberAsyncImagePainter
@@ -45,6 +42,25 @@ fun CIImageView(
showMenu: MutableState<Boolean>, showMenu: MutableState<Boolean>,
receiveFile: (Long) -> Unit receiveFile: (Long) -> Unit
) { ) {
@Composable
fun progressIndicator() {
CircularProgressIndicator(
Modifier.size(16.dp),
color = Color.White,
strokeWidth = 2.dp
)
}
@Composable
fun fileIcon(icon: Painter, @StringRes stringId: Int) {
Icon(
icon,
stringResource(stringId),
Modifier.fillMaxSize(),
tint = Color.White
)
}
@Composable @Composable
fun loadingIndicator() { fun loadingIndicator() {
if (file != null) { if (file != null) {
@@ -55,39 +71,20 @@ fun CIImageView(
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
when (file.fileStatus) { when (file.fileStatus) {
CIFileStatus.SndTransfer -> is CIFileStatus.SndStored ->
CircularProgressIndicator( when (file.fileProtocol) {
Modifier.size(16.dp), FileProtocol.XFTP -> progressIndicator()
color = Color.White, FileProtocol.SMP -> {}
strokeWidth = 2.dp }
) is CIFileStatus.SndTransfer -> progressIndicator()
CIFileStatus.SndComplete -> is CIFileStatus.SndComplete -> fileIcon(painterResource(R.drawable.ic_check_filled), R.string.icon_descr_image_snd_complete)
Icon( is CIFileStatus.SndCancelled -> fileIcon(painterResource(R.drawable.ic_close), R.string.icon_descr_file)
Icons.Filled.Check, is CIFileStatus.SndError -> fileIcon(painterResource(R.drawable.ic_close), R.string.icon_descr_file)
stringResource(R.string.icon_descr_image_snd_complete), is CIFileStatus.RcvInvitation -> fileIcon(painterResource(R.drawable.ic_arrow_downward), R.string.icon_descr_asked_to_receive)
Modifier.fillMaxSize(), is CIFileStatus.RcvAccepted -> fileIcon(painterResource(R.drawable.ic_more_horiz), R.string.icon_descr_waiting_for_image)
tint = Color.White is CIFileStatus.RcvTransfer -> progressIndicator()
) is CIFileStatus.RcvCancelled -> fileIcon(painterResource(R.drawable.ic_close), R.string.icon_descr_file)
CIFileStatus.RcvAccepted -> is CIFileStatus.RcvError -> fileIcon(painterResource(R.drawable.ic_close), R.string.icon_descr_file)
Icon(
Icons.Outlined.MoreHoriz,
stringResource(R.string.icon_descr_waiting_for_image),
Modifier.fillMaxSize(),
tint = Color.White
)
CIFileStatus.RcvTransfer ->
CircularProgressIndicator(
Modifier.size(16.dp),
color = Color.White,
strokeWidth = 2.dp
)
CIFileStatus.RcvInvitation ->
Icon(
Icons.Outlined.ArrowDownward,
stringResource(R.string.icon_descr_asked_to_receive),
Modifier.fillMaxSize(),
tint = Color.White
)
else -> {} else -> {}
} }
} }
@@ -136,7 +133,7 @@ fun CIImageView(
fun fileSizeValid(): Boolean { fun fileSizeValid(): Boolean {
if (file != null) { if (file != null) {
return file.fileSize <= MAX_FILE_SIZE return file.fileSize <= getMaxFileSize(file.fileProtocol)
} }
return false return false
} }
@@ -179,15 +176,23 @@ fun CIImageView(
} else { } else {
AlertManager.shared.showAlertMsg( AlertManager.shared.showAlertMsg(
generalGetString(R.string.large_file), generalGetString(R.string.large_file),
String.format(generalGetString(R.string.contact_sent_large_file), formatBytes(MAX_FILE_SIZE)) String.format(generalGetString(R.string.contact_sent_large_file), formatBytes(getMaxFileSize(file.fileProtocol)))
) )
} }
CIFileStatus.RcvAccepted -> CIFileStatus.RcvAccepted ->
AlertManager.shared.showAlertMsg( when (file.fileProtocol) {
generalGetString(R.string.waiting_for_image), FileProtocol.XFTP ->
generalGetString(R.string.image_will_be_received_when_contact_is_online) AlertManager.shared.showAlertMsg(
) generalGetString(R.string.waiting_for_image),
CIFileStatus.RcvTransfer -> {} // ? generalGetString(R.string.image_will_be_received_when_contact_completes_uploading)
)
FileProtocol.SMP ->
AlertManager.shared.showAlertMsg(
generalGetString(R.string.waiting_for_image),
generalGetString(R.string.image_will_be_received_when_contact_is_online)
)
}
CIFileStatus.RcvTransfer(rcvProgress = 7, rcvTotal = 10) -> {} // ?
CIFileStatus.RcvComplete -> {} // ? CIFileStatus.RcvComplete -> {} // ?
CIFileStatus.RcvCancelled -> {} // TODO CIFileStatus.RcvCancelled -> {} // TODO
else -> {} else -> {}
@@ -5,12 +5,11 @@ import SectionView
import androidx.compose.foundation.* import androidx.compose.foundation.*
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.material.* import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Share
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@@ -35,7 +34,7 @@ fun InvalidJSONView(json: String) {
Spacer(Modifier.height(DEFAULT_PADDING)) Spacer(Modifier.height(DEFAULT_PADDING))
SectionView { SectionView {
val context = LocalContext.current val context = LocalContext.current
SettingsActionItem(Icons.Outlined.Share, generalGetString(R.string.share_verb), click = { SettingsActionItem(painterResource(R.drawable.ic_share), generalGetString(R.string.share_verb), click = {
shareText(context, json) shareText(context, json)
}) })
} }
@@ -2,24 +2,23 @@ package chat.simplex.app.views.chat.item
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.material.* import androidx.compose.material.*
import androidx.compose.material.icons.Icons import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.material.icons.filled.Circle import androidx.compose.ui.res.painterResource
import androidx.compose.material.icons.outlined.Edit import chat.simplex.app.R
import androidx.compose.material.icons.outlined.Timer
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import chat.simplex.app.model.* import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.HighOrLowlight import chat.simplex.app.ui.theme.CurrentColors
import kotlinx.datetime.Clock import kotlinx.datetime.Clock
@Composable @Composable
fun CIMetaView(chatItem: ChatItem, timedMessagesTTL: Int?, metaColor: Color = HighOrLowlight) { fun CIMetaView(chatItem: ChatItem, timedMessagesTTL: Int?, metaColor: Color = MaterialTheme.colors.secondary) {
Row(Modifier.padding(start = 3.dp), verticalAlignment = Alignment.CenterVertically) { Row(Modifier.padding(start = 3.dp), verticalAlignment = Alignment.CenterVertically) {
if (chatItem.isDeletedContent) { if (chatItem.isDeletedContent) {
Text( Text(
@@ -38,11 +37,11 @@ fun CIMetaView(chatItem: ChatItem, timedMessagesTTL: Int?, metaColor: Color = Hi
// changing this function requires updating reserveSpaceForMeta // changing this function requires updating reserveSpaceForMeta
private fun CIMetaText(meta: CIMeta, chatTTL: Int?, color: Color) { private fun CIMetaText(meta: CIMeta, chatTTL: Int?, color: Color) {
if (meta.itemEdited) { if (meta.itemEdited) {
StatusIconText(Icons.Outlined.Edit, color) StatusIconText(painterResource(R.drawable.ic_edit), color)
Spacer(Modifier.width(3.dp)) Spacer(Modifier.width(3.dp))
} }
if (meta.disappearing) { if (meta.disappearing) {
StatusIconText(Icons.Outlined.Timer, color) StatusIconText(painterResource(R.drawable.ic_timer), color)
val ttl = meta.itemTimed?.ttl val ttl = meta.itemTimed?.ttl
if (ttl != chatTTL) { if (ttl != chatTTL) {
Text(TimedMessagesPreference.shortTtlText(ttl), color = color, fontSize = 13.sp) Text(TimedMessagesPreference.shortTtlText(ttl), color = color, fontSize = 13.sp)
@@ -52,13 +51,13 @@ private fun CIMetaText(meta: CIMeta, chatTTL: Int?, color: Color) {
val statusIcon = meta.statusIcon(MaterialTheme.colors.primary, color) val statusIcon = meta.statusIcon(MaterialTheme.colors.primary, color)
if (statusIcon != null) { if (statusIcon != null) {
val (icon, statusColor) = statusIcon val (icon, statusColor) = statusIcon
StatusIconText(icon, statusColor) StatusIconText(painterResource(icon), statusColor)
Spacer(Modifier.width(4.dp)) Spacer(Modifier.width(4.dp))
} else if (!meta.disappearing) { } else if (!meta.disappearing) {
StatusIconText(Icons.Filled.Circle, Color.Transparent) StatusIconText(painterResource(R.drawable.ic_circle_filled), Color.Transparent)
Spacer(Modifier.width(4.dp)) Spacer(Modifier.width(4.dp))
} }
Text(meta.timestampText, color = color, fontSize = 13.sp) Text(meta.timestampText, color = color, fontSize = 13.sp, maxLines = 1, overflow = TextOverflow.Ellipsis)
} }
// the conditions in this function should match CIMetaText // the conditions in this function should match CIMetaText
@@ -73,14 +72,14 @@ fun reserveSpaceForMeta(meta: CIMeta, chatTTL: Int?): String {
res += TimedMessagesPreference.shortTtlText(ttl) res += TimedMessagesPreference.shortTtlText(ttl)
} }
} }
if (meta.statusIcon(HighOrLowlight) != null || !meta.disappearing) { if (meta.statusIcon(CurrentColors.value.colors.secondary) != null || !meta.disappearing) {
res += iconSpace res += iconSpace
} }
return res + meta.timestampText return res + meta.timestampText
} }
@Composable @Composable
private fun StatusIconText(icon: ImageVector, color: Color) { private fun StatusIconText(icon: Painter, color: Color) {
Icon(icon, null, Modifier.height(12.dp), tint = color) Icon(icon, null, Modifier.height(12.dp), tint = color)
} }
@@ -0,0 +1,24 @@
package chat.simplex.app.views.chat.item
import androidx.compose.runtime.Composable
import chat.simplex.app.R
import chat.simplex.app.model.*
import chat.simplex.app.views.helpers.AlertManager
import chat.simplex.app.views.helpers.generalGetString
@Composable
fun CIRcvDecryptionError(msgDecryptError: MsgDecryptError, msgCount: UInt, ci: ChatItem, timedMessagesTTL: Int?, showMember: Boolean) {
CIMsgError(ci, timedMessagesTTL, showMember) {
AlertManager.shared.showAlertMsg(
title = generalGetString(R.string.decryption_error),
text = when (msgDecryptError) {
MsgDecryptError.RatchetHeader -> String.format(generalGetString(R.string.alert_text_decryption_error_header), msgCount.toLong()) + "\n" +
generalGetString(R.string.alert_text_fragment_encryption_out_of_sync_old_database) + "\n" +
generalGetString(R.string.alert_text_fragment_permanent_error_reconnect)
MsgDecryptError.TooManySkipped -> String.format(generalGetString(R.string.alert_text_decryption_error_too_many_skipped), msgCount.toLong()) + "\n" +
generalGetString(R.string.alert_text_fragment_encryption_out_of_sync_old_database) + "\n" +
generalGetString(R.string.alert_text_fragment_permanent_error_reconnect)
}
)
}
}
@@ -0,0 +1,338 @@
package chat.simplex.app.views.chat.item
import android.graphics.Bitmap
import android.graphics.Rect
import android.net.Uri
import androidx.annotation.StringRes
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CornerSize
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.*
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.layout.*
import androidx.compose.ui.platform.*
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.*
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.content.FileProvider
import androidx.core.graphics.drawable.toDrawable
import chat.simplex.app.*
import chat.simplex.app.R
import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.helpers.*
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout.RESIZE_MODE_FIXED_WIDTH
import com.google.android.exoplayer2.ui.StyledPlayerView
import java.io.File
@Composable
fun CIVideoView(
image: String,
duration: Int,
file: CIFile?,
imageProvider: () -> ImageGalleryProvider,
showMenu: MutableState<Boolean>,
receiveFile: (Long) -> Unit
) {
Box(
Modifier.layoutId(CHAT_IMAGE_LAYOUT_ID),
contentAlignment = Alignment.TopEnd
) {
val context = LocalContext.current
val filePath = remember(file) { getLoadedFilePath(SimplexApp.context, file) }
val preview = remember(image) { base64ToBitmap(image) }
if (file != null && filePath != null) {
val uri = remember(filePath) { FileProvider.getUriForFile(context, "${BuildConfig.APPLICATION_ID}.provider", File(filePath)) }
val view = LocalView.current
VideoView(uri, file, preview, duration * 1000L, showMenu, onClick = {
hideKeyboard(view)
ModalManager.shared.showCustomModal(animated = false) { close ->
ImageFullScreenView(imageProvider, close)
}
})
} else {
Box {
ImageView(preview, showMenu, onClick = {
if (file != null) {
when (file.fileStatus) {
CIFileStatus.RcvInvitation ->
receiveFileIfValidSize(file, receiveFile)
CIFileStatus.RcvAccepted ->
when (file.fileProtocol) {
FileProtocol.XFTP ->
AlertManager.shared.showAlertMsg(
generalGetString(R.string.waiting_for_video),
generalGetString(R.string.video_will_be_received_when_contact_completes_uploading)
)
FileProtocol.SMP ->
AlertManager.shared.showAlertMsg(
generalGetString(R.string.waiting_for_video),
generalGetString(R.string.video_will_be_received_when_contact_is_online)
)
}
CIFileStatus.RcvTransfer(rcvProgress = 7, rcvTotal = 10) -> {} // ?
CIFileStatus.RcvComplete -> {} // ?
CIFileStatus.RcvCancelled -> {} // TODO
else -> {}
}
}
})
if (file != null) {
DurationProgress(file, remember { mutableStateOf(false) }, remember { mutableStateOf(duration * 1000L) }, remember { mutableStateOf(0L) }/*, soundEnabled*/)
}
if (file?.fileStatus is CIFileStatus.RcvInvitation) {
PlayButton(error = false, { showMenu.value = true }) { receiveFileIfValidSize(file, receiveFile) }
}
}
}
loadingIndicator(file)
}
}
@Composable
private fun VideoView(uri: Uri, file: CIFile, defaultPreview: Bitmap, defaultDuration: Long, showMenu: MutableState<Boolean>, onClick: () -> Unit) {
val context = LocalContext.current
val player = remember(uri) { VideoPlayer.getOrCreate(uri, false, defaultPreview, defaultDuration, true, context) }
val videoPlaying = remember(uri.path) { player.videoPlaying }
val progress = remember(uri.path) { player.progress }
val duration = remember(uri.path) { player.duration }
val preview by remember { player.preview }
// val soundEnabled by rememberSaveable(uri.path) { player.soundEnabled }
val brokenVideo by rememberSaveable(uri.path) { player.brokenVideo }
val play = {
player.enableSound(true)
player.play(true)
}
val stop = {
player.enableSound(false)
player.stop()
}
val showPreview = remember { derivedStateOf { !videoPlaying.value || progress.value == 0L } }
DisposableEffect(Unit) {
onDispose {
stop()
}
}
Box {
val windowWidth = LocalWindowWidth()
val width = remember(preview) { if (preview.width * 0.97 <= preview.height) videoViewFullWidth(windowWidth) * 0.75f else 1000.dp }
AndroidView(
factory = { ctx ->
StyledPlayerView(ctx).apply {
useController = false
resizeMode = RESIZE_MODE_FIXED_WIDTH
this.player = player.player
}
},
Modifier
.width(width)
.combinedClickable(
onLongClick = { showMenu.value = true },
onClick = { if (player.player.playWhenReady) stop() else onClick() }
)
)
if (showPreview.value) {
ImageView(preview, showMenu, onClick)
PlayButton(brokenVideo, onLongClick = { showMenu.value = true }, play)
}
DurationProgress(file, videoPlaying, duration, progress/*, soundEnabled*/)
}
}
@Composable
private fun BoxScope.PlayButton(error: Boolean = false, onLongClick: () -> Unit, onClick: () -> Unit) {
Surface(
Modifier.align(Alignment.Center),
color = Color.Black.copy(alpha = 0.25f),
shape = RoundedCornerShape(percent = 50)
) {
Box(
Modifier
.defaultMinSize(minWidth = 40.dp, minHeight = 40.dp)
.combinedClickable(onClick = onClick, onLongClick = onLongClick),
contentAlignment = Alignment.Center
) {
Icon(
painterResource(R.drawable.ic_play_arrow_filled),
contentDescription = null,
tint = if (error) WarningOrange else Color.White
)
}
}
}
@Composable
private fun DurationProgress(file: CIFile, playing: MutableState<Boolean>, duration: MutableState<Long>, progress: MutableState<Long>/*, soundEnabled: MutableState<Boolean>*/) {
if (duration.value > 0L || progress.value > 0) {
Row {
Box(
Modifier
.padding(DEFAULT_PADDING_HALF)
.background(Color.Black.copy(alpha = 0.35f), RoundedCornerShape(percent = 50))
.padding(vertical = 2.dp, horizontal = 4.dp)
) {
val time = if (progress.value > 0) progress.value else duration.value
val timeStr = durationText((time / 1000).toInt())
val width = if (timeStr.length <= 5) 44 else 50
Text(
timeStr,
Modifier.widthIn(min = with(LocalDensity.current) { width.sp.toDp() }).padding(horizontal = 4.dp),
fontSize = 13.sp,
color = Color.White
)
/*if (!soundEnabled.value) {
Icon(painterResource(R.drawable.ic_volume_off_filled), null,
Modifier.padding(start = 5.dp).size(10.dp),
tint = Color.White
)
}*/
}
if (!playing.value) {
Box(
Modifier
.padding(top = DEFAULT_PADDING_HALF)
.background(Color.Black.copy(alpha = 0.35f), RoundedCornerShape(percent = 50))
.padding(vertical = 2.dp, horizontal = 4.dp)
) {
Text(
formatBytes(file.fileSize),
Modifier.padding(horizontal = 4.dp),
fontSize = 13.sp,
color = Color.White
)
}
}
}
}
}
@Composable
private fun ImageView(preview: Bitmap, showMenu: MutableState<Boolean>, onClick: () -> Unit) {
val windowWidth = LocalWindowWidth()
val width = remember(preview) { if (preview.width * 0.97 <= preview.height) videoViewFullWidth(windowWidth) * 0.75f else 1000.dp }
Image(
preview.asImageBitmap(),
contentDescription = stringResource(R.string.video_descr),
modifier = Modifier
.width(width)
.combinedClickable(
onLongClick = { showMenu.value = true },
onClick = onClick
),
contentScale = ContentScale.FillWidth,
)
}
@Composable
private fun LocalWindowWidth(): Dp {
val view = LocalView.current
val density = LocalDensity.current.density
return remember {
val rect = Rect()
view.getWindowVisibleDisplayFrame(rect)
(rect.width() / density).dp
}
}
@Composable
private fun progressIndicator() {
CircularProgressIndicator(
Modifier.size(16.dp),
color = Color.White,
strokeWidth = 2.dp
)
}
@Composable
private fun fileIcon(icon: Painter, @StringRes stringId: Int) {
Icon(
icon,
stringResource(stringId),
Modifier.fillMaxSize(),
tint = Color.White
)
}
@Composable
private fun progressCircle(progress: Long, total: Long) {
val angle = 360f * (progress.toDouble() / total.toDouble()).toFloat()
val strokeWidth = with(LocalDensity.current) { 2.dp.toPx() }
val strokeColor = Color.White
Surface(
Modifier.drawRingModifier(angle, strokeColor, strokeWidth),
color = Color.Transparent,
shape = MaterialTheme.shapes.small.copy(CornerSize(percent = 50))
) {
Box(Modifier.size(16.dp))
}
}
@Composable
private fun loadingIndicator(file: CIFile?) {
if (file != null) {
Box(
Modifier
.padding(8.dp)
.size(20.dp),
contentAlignment = Alignment.Center
) {
when (file.fileStatus) {
is CIFileStatus.SndStored ->
when (file.fileProtocol) {
FileProtocol.XFTP -> progressIndicator()
FileProtocol.SMP -> {}
}
is CIFileStatus.SndTransfer ->
when (file.fileProtocol) {
FileProtocol.XFTP -> progressCircle(file.fileStatus.sndProgress, file.fileStatus.sndTotal)
FileProtocol.SMP -> progressIndicator()
}
is CIFileStatus.SndComplete -> fileIcon(painterResource(R.drawable.ic_check_filled), R.string.icon_descr_video_snd_complete)
is CIFileStatus.SndCancelled -> fileIcon(painterResource(R.drawable.ic_close), R.string.icon_descr_file)
is CIFileStatus.SndError -> fileIcon(painterResource(R.drawable.ic_close), R.string.icon_descr_file)
is CIFileStatus.RcvInvitation -> fileIcon(painterResource(R.drawable.ic_arrow_downward), R.string.icon_descr_video_asked_to_receive)
is CIFileStatus.RcvAccepted -> fileIcon(painterResource(R.drawable.ic_more_horiz), R.string.icon_descr_waiting_for_video)
is CIFileStatus.RcvTransfer ->
if (file.fileProtocol == FileProtocol.XFTP && file.fileStatus.rcvProgress < file.fileStatus.rcvTotal) {
progressCircle(file.fileStatus.rcvProgress, file.fileStatus.rcvTotal)
} else {
progressIndicator()
}
is CIFileStatus.RcvCancelled -> fileIcon(painterResource(R.drawable.ic_close), R.string.icon_descr_file)
is CIFileStatus.RcvError -> fileIcon(painterResource(R.drawable.ic_close), R.string.icon_descr_file)
else -> {}
}
}
}
}
private fun fileSizeValid(file: CIFile?): Boolean {
if (file != null) {
return file.fileSize <= getMaxFileSize(file.fileProtocol)
}
return false
}
private fun receiveFileIfValidSize(file: CIFile, receiveFile: (Long) -> Unit) {
if (fileSizeValid(file)) {
receiveFile(file.fileId)
} else {
AlertManager.shared.showAlertMsg(
generalGetString(R.string.large_file),
String.format(generalGetString(R.string.contact_sent_large_file), formatBytes(getMaxFileSize(file.fileProtocol)))
)
}
}
private fun videoViewFullWidth(windowWidth: Dp): Dp {
val approximatePadding = 100.dp
return minOf(1000.dp, windowWidth - approximatePadding)
}
@@ -5,8 +5,6 @@ import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CornerSize import androidx.compose.foundation.shape.CornerSize
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.* import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
@@ -19,7 +17,9 @@ import androidx.compose.ui.graphics.*
import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.* import androidx.compose.ui.unit.*
import chat.simplex.app.R
import chat.simplex.app.model.* import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.* import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.helpers.* import chat.simplex.app.views.helpers.*
@@ -137,7 +137,7 @@ private fun DurationText(text: State<String>, padding: PaddingValues) {
Modifier Modifier
.padding(padding) .padding(padding)
.widthIn(min = minWidth), .widthIn(min = minWidth),
color = HighOrLowlight, color = MaterialTheme.colors.secondary,
fontSize = 16.sp, fontSize = 16.sp,
maxLines = 1 maxLines = 1
) )
@@ -156,9 +156,11 @@ private fun PlayPauseButton(
pause: () -> Unit, pause: () -> Unit,
longClick: () -> Unit longClick: () -> Unit
) { ) {
val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage
val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage
Surface( Surface(
Modifier.drawRingModifier(angle, strokeColor, strokeWidth), Modifier.drawRingModifier(angle, strokeColor, strokeWidth),
color = if (sent) SentColorLight else ReceivedColorLight, color = if (sent) sentColor else receivedColor,
shape = MaterialTheme.shapes.small.copy(CornerSize(percent = 50)) shape = MaterialTheme.shapes.small.copy(CornerSize(percent = 50))
) { ) {
Box( Box(
@@ -171,10 +173,10 @@ private fun PlayPauseButton(
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
Icon( Icon(
imageVector = if (audioPlaying) Icons.Filled.Pause else Icons.Filled.PlayArrow, if (audioPlaying) painterResource(R.drawable.ic_pause_filled) else painterResource(R.drawable.ic_play_arrow_filled),
contentDescription = null, contentDescription = null,
Modifier.size(36.dp), Modifier.size(36.dp),
tint = if (error) WarningOrange else if (!enabled) HighOrLowlight else MaterialTheme.colors.primary tint = if (error) WarningOrange else if (!enabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary
) )
} }
} }
@@ -200,7 +202,7 @@ private fun VoiceMsgIndicator(
if (hasText) { if (hasText) {
IconButton({ if (!audioPlaying) play() else pause() }, Modifier.size(56.dp).drawRingModifier(angle, strokeColor, strokeWidth)) { IconButton({ if (!audioPlaying) play() else pause() }, Modifier.size(56.dp).drawRingModifier(angle, strokeColor, strokeWidth)) {
Icon( Icon(
imageVector = if (audioPlaying) Icons.Filled.Pause else Icons.Filled.PlayArrow, if (audioPlaying) painterResource(R.drawable.ic_pause_filled) else painterResource(R.drawable.ic_play_arrow_filled),
contentDescription = null, contentDescription = null,
Modifier.size(36.dp), Modifier.size(36.dp),
tint = MaterialTheme.colors.primary tint = MaterialTheme.colors.primary
@@ -210,9 +212,9 @@ private fun VoiceMsgIndicator(
PlayPauseButton(audioPlaying, sent, angle, strokeWidth, strokeColor, true, error, play, pause, longClick = longClick) PlayPauseButton(audioPlaying, sent, angle, strokeWidth, strokeColor, true, error, play, pause, longClick = longClick)
} }
} else { } else {
if (file?.fileStatus == CIFileStatus.RcvInvitation if (file?.fileStatus is CIFileStatus.RcvInvitation
|| file?.fileStatus == CIFileStatus.RcvTransfer || file?.fileStatus is CIFileStatus.RcvTransfer
|| file?.fileStatus == CIFileStatus.RcvAccepted || file?.fileStatus is CIFileStatus.RcvAccepted
) { ) {
Box( Box(
Modifier Modifier
@@ -228,7 +230,7 @@ private fun VoiceMsgIndicator(
} }
} }
private fun Modifier.drawRingModifier(angle: Float, color: Color, strokeWidth: Float) = drawWithCache { fun Modifier.drawRingModifier(angle: Float, color: Color, strokeWidth: Float) = drawWithCache {
val brush = Brush.linearGradient( val brush = Brush.linearGradient(
0f to Color.Transparent, 0f to Color.Transparent,
0f to color, 0f to color,
@@ -1,30 +1,31 @@
package chat.simplex.app.views.chat.item package chat.simplex.app.views.chat.item
import android.Manifest
import android.os.Build
import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.* import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material.icons.outlined.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.* import androidx.compose.ui.platform.*
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import chat.simplex.app.* import chat.simplex.app.*
import chat.simplex.app.R import chat.simplex.app.R
import chat.simplex.app.model.* import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.HighOrLowlight import chat.simplex.app.ui.theme.*
import chat.simplex.app.ui.theme.SimpleXTheme
import chat.simplex.app.views.chat.ComposeContextItem import chat.simplex.app.views.chat.ComposeContextItem
import chat.simplex.app.views.chat.ComposeState import chat.simplex.app.views.chat.ComposeState
import chat.simplex.app.views.helpers.* import chat.simplex.app.views.helpers.*
import com.google.accompanist.permissions.rememberPermissionState
import kotlinx.datetime.Clock import kotlinx.datetime.Clock
// TODO refactor so that FramedItemView can show all CIContent items if they're deleted (see Swift code) // TODO refactor so that FramedItemView can show all CIContent items if they're deleted (see Swift code)
@@ -40,6 +41,7 @@ fun ChatItemView(
linkMode: SimplexLinkMode, linkMode: SimplexLinkMode,
deleteMessage: (Long, CIDeleteMode) -> Unit, deleteMessage: (Long, CIDeleteMode) -> Unit,
receiveFile: (Long) -> Unit, receiveFile: (Long) -> Unit,
cancelFile: (Long) -> Unit,
joinGroup: (Long) -> Unit, joinGroup: (Long) -> Unit,
acceptCall: (Contact) -> Unit, acceptCall: (Contact) -> Unit,
scrollToItem: (Long) -> Unit, scrollToItem: (Long) -> Unit,
@@ -91,15 +93,19 @@ fun ChatItemView(
} }
} }
fun moderateMessageQuestionText(): String {
return if (fullDeleteAllowed) {
generalGetString(R.string.moderate_message_will_be_deleted_warning)
} else {
generalGetString(R.string.moderate_message_will_be_marked_warning)
}
}
@Composable @Composable
fun MsgContentItemDropdownMenu() { fun MsgContentItemDropdownMenu() {
DropdownMenu( DefaultDropdownMenu(showMenu) {
expanded = showMenu.value, if (cItem.meta.itemDeleted == null && !live) {
onDismissRequest = { showMenu.value = false }, ItemAction(stringResource(R.string.reply_verb), painterResource(R.drawable.ic_reply), onClick = {
Modifier.width(220.dp)
) {
if (!cItem.meta.itemDeleted && !live) {
ItemAction(stringResource(R.string.reply_verb), Icons.Outlined.Reply, onClick = {
if (composeState.value.editing) { if (composeState.value.editing) {
composeState.value = ComposeState(contextItem = ComposeContextItem.QuotedItem(cItem), useLinkPreviews = useLinkPreviews) composeState.value = ComposeState(contextItem = ComposeContextItem.QuotedItem(cItem), useLinkPreviews = useLinkPreviews)
} else { } else {
@@ -108,7 +114,7 @@ fun ChatItemView(
showMenu.value = false showMenu.value = false
}) })
} }
ItemAction(stringResource(R.string.share_verb), Icons.Outlined.Share, onClick = { ItemAction(stringResource(R.string.share_verb), painterResource(R.drawable.ic_share), onClick = {
val filePath = getLoadedFilePath(SimplexApp.context, cItem.file) val filePath = getLoadedFilePath(SimplexApp.context, cItem.file)
when { when {
filePath != null -> shareFile(context, cItem.text, filePath) filePath != null -> shareFile(context, cItem.text, filePath)
@@ -116,18 +122,24 @@ fun ChatItemView(
} }
showMenu.value = false showMenu.value = false
}) })
ItemAction(stringResource(R.string.copy_verb), Icons.Outlined.ContentCopy, onClick = { ItemAction(stringResource(R.string.copy_verb), painterResource(R.drawable.ic_content_copy), onClick = {
copyText(context, cItem.content.text) copyText(context, cItem.content.text)
showMenu.value = false showMenu.value = false
}) })
if (cItem.content.msgContent is MsgContent.MCImage || cItem.content.msgContent is MsgContent.MCFile || cItem.content.msgContent is MsgContent.MCVoice) { if (cItem.content.msgContent is MsgContent.MCImage || cItem.content.msgContent is MsgContent.MCVideo || cItem.content.msgContent is MsgContent.MCFile || cItem.content.msgContent is MsgContent.MCVoice) {
val filePath = getLoadedFilePath(context, cItem.file) val filePath = getLoadedFilePath(context, cItem.file)
if (filePath != null) { if (filePath != null) {
ItemAction(stringResource(R.string.save_verb), Icons.Outlined.SaveAlt, onClick = { val writePermissionState = rememberPermissionState(permission = Manifest.permission.WRITE_EXTERNAL_STORAGE)
ItemAction(stringResource(R.string.save_verb), painterResource(R.drawable.ic_download), onClick = {
when (cItem.content.msgContent) { when (cItem.content.msgContent) {
is MsgContent.MCImage -> saveImage(context, cItem.file) is MsgContent.MCImage -> {
is MsgContent.MCFile -> saveFileLauncher.launch(cItem.file?.fileName) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R || writePermissionState.hasPermission) {
is MsgContent.MCVoice -> saveFileLauncher.launch(cItem.file?.fileName) saveImage(context, cItem.file)
} else {
writePermissionState.launchPermissionRequest()
}
}
is MsgContent.MCFile, is MsgContent.MCVoice, is MsgContent.MCVideo -> saveFileLauncher.launch(cItem.file?.fileName)
else -> {} else -> {}
} }
showMenu.value = false showMenu.value = false
@@ -135,42 +147,47 @@ fun ChatItemView(
} }
} }
if (cItem.meta.editable && cItem.content.msgContent !is MsgContent.MCVoice && !live) { if (cItem.meta.editable && cItem.content.msgContent !is MsgContent.MCVoice && !live) {
ItemAction(stringResource(R.string.edit_verb), Icons.Filled.Edit, onClick = { ItemAction(stringResource(R.string.edit_verb), painterResource(R.drawable.ic_edit_filled), onClick = {
composeState.value = ComposeState(editingItem = cItem, useLinkPreviews = useLinkPreviews) composeState.value = ComposeState(editingItem = cItem, useLinkPreviews = useLinkPreviews)
showMenu.value = false showMenu.value = false
}) })
} }
if (cItem.meta.itemDeleted && revealed.value) { if (cItem.meta.itemDeleted != null && revealed.value) {
ItemAction( ItemAction(
stringResource(R.string.hide_verb), stringResource(R.string.hide_verb),
Icons.Outlined.VisibilityOff, painterResource(R.drawable.ic_visibility_off),
onClick = { onClick = {
revealed.value = false revealed.value = false
showMenu.value = false showMenu.value = false
} }
) )
} }
if (cItem.meta.itemDeleted == null && cItem.file != null && cItem.file.cancelAction != null) {
CancelFileItemAction(cItem.file.fileId, showMenu, cancelFile = cancelFile, cancelAction = cItem.file.cancelAction)
}
if (!(live && cItem.meta.isLive)) { if (!(live && cItem.meta.isLive)) {
DeleteItemAction(cItem, showMenu, questionText = deleteMessageQuestionText(), deleteMessage) DeleteItemAction(cItem, showMenu, questionText = deleteMessageQuestionText(), deleteMessage)
} }
val groupInfo = cItem.memberToModerate(cInfo)?.first
if (groupInfo != null) {
ModerateItemAction(cItem, questionText = moderateMessageQuestionText(), showMenu, deleteMessage)
}
} }
} }
@Composable @Composable
fun MarkedDeletedItemDropdownMenu() { fun MarkedDeletedItemDropdownMenu() {
DropdownMenu( DefaultDropdownMenu(showMenu) {
expanded = showMenu.value, if (!cItem.isDeletedContent) {
onDismissRequest = { showMenu.value = false }, ItemAction(
Modifier.width(220.dp) stringResource(R.string.reveal_verb),
) { painterResource(R.drawable.ic_visibility),
ItemAction( onClick = {
stringResource(R.string.reveal_verb), revealed.value = true
Icons.Outlined.Visibility, showMenu.value = false
onClick = { }
revealed.value = true )
showMenu.value = false }
}
)
DeleteItemAction(cItem, showMenu, questionText = deleteMessageQuestionText(), deleteMessage) DeleteItemAction(cItem, showMenu, questionText = deleteMessageQuestionText(), deleteMessage)
} }
} }
@@ -178,10 +195,10 @@ fun ChatItemView(
@Composable @Composable
fun ContentItem() { fun ContentItem() {
val mc = cItem.content.msgContent val mc = cItem.content.msgContent
if (cItem.meta.itemDeleted && !revealed.value) { if (cItem.meta.itemDeleted != null && !revealed.value) {
MarkedDeletedItemView(cItem, cInfo.timedMessagesTTL, showMember = showMember) MarkedDeletedItemView(cItem, cInfo.timedMessagesTTL, showMember = showMember)
MarkedDeletedItemDropdownMenu() MarkedDeletedItemDropdownMenu()
} else if (cItem.quotedItem == null && !cItem.meta.itemDeleted && !cItem.meta.isLive) { } else if (cItem.quotedItem == null && cItem.meta.itemDeleted == null && !cItem.meta.isLive) {
if (mc is MsgContent.MCText && isShortEmoji(cItem.content.text)) { if (mc is MsgContent.MCText && isShortEmoji(cItem.content.text)) {
EmojiItemView(cItem, cInfo.timedMessagesTTL) EmojiItemView(cItem, cInfo.timedMessagesTTL)
MsgContentItemDropdownMenu() MsgContentItemDropdownMenu()
@@ -200,11 +217,7 @@ fun ChatItemView(
@Composable fun DeletedItem() { @Composable fun DeletedItem() {
DeletedItemView(cItem, cInfo.timedMessagesTTL, showMember = showMember) DeletedItemView(cItem, cInfo.timedMessagesTTL, showMember = showMember)
DropdownMenu( DefaultDropdownMenu(showMenu) {
expanded = showMenu.value,
onDismissRequest = { showMenu.value = false },
Modifier.width(220.dp)
) {
DeleteItemAction(cItem, showMenu, questionText = deleteMessageQuestionText(), deleteMessage) DeleteItemAction(cItem, showMenu, questionText = deleteMessageQuestionText(), deleteMessage)
} }
} }
@@ -220,7 +233,8 @@ fun ChatItemView(
is CIContent.RcvDeleted -> DeletedItem() is CIContent.RcvDeleted -> DeletedItem()
is CIContent.SndCall -> CallItem(c.status, c.duration) is CIContent.SndCall -> CallItem(c.status, c.duration)
is CIContent.RcvCall -> CallItem(c.status, c.duration) is CIContent.RcvCall -> CallItem(c.status, c.duration)
is CIContent.RcvIntegrityError -> IntegrityErrorItemView(cItem, cInfo.timedMessagesTTL, showMember = showMember) is CIContent.RcvIntegrityError -> IntegrityErrorItemView(c.msgError, cItem, cInfo.timedMessagesTTL, showMember = showMember)
is CIContent.RcvDecryptionError -> CIRcvDecryptionError(c.msgDecryptError, c.msgCount, cItem, cInfo.timedMessagesTTL, showMember = showMember)
is CIContent.RcvGroupInvitation -> CIGroupInvitationView(cItem, c.groupInvitation, c.memberRole, joinGroup = joinGroup, chatIncognito = cInfo.incognito) is CIContent.RcvGroupInvitation -> CIGroupInvitationView(cItem, c.groupInvitation, c.memberRole, joinGroup = joinGroup, chatIncognito = cInfo.incognito)
is CIContent.SndGroupInvitation -> CIGroupInvitationView(cItem, c.groupInvitation, c.memberRole, joinGroup = joinGroup, chatIncognito = cInfo.incognito) is CIContent.SndGroupInvitation -> CIGroupInvitationView(cItem, c.groupInvitation, c.memberRole, joinGroup = joinGroup, chatIncognito = cInfo.incognito)
is CIContent.RcvGroupEventContent -> CIEventView(cItem) is CIContent.RcvGroupEventContent -> CIEventView(cItem)
@@ -233,17 +247,37 @@ fun ChatItemView(
val ct = if (cInfo is ChatInfo.Direct) cInfo.contact else null val ct = if (cInfo is ChatInfo.Direct) cInfo.contact else null
CIFeaturePreferenceView(cItem, ct, c.feature, c.allowed, acceptFeature) CIFeaturePreferenceView(cItem, ct, c.feature, c.allowed, acceptFeature)
} }
is CIContent.SndChatPreference -> CIChatFeatureView(cItem, c.feature, HighOrLowlight, icon = c.feature.icon,) is CIContent.SndChatPreference -> CIChatFeatureView(cItem, c.feature, MaterialTheme.colors.secondary, icon = c.feature.icon,)
is CIContent.RcvGroupFeature -> CIChatFeatureView(cItem, c.groupFeature, c.preference.enable.iconColor) is CIContent.RcvGroupFeature -> CIChatFeatureView(cItem, c.groupFeature, c.preference.enable.iconColor)
is CIContent.SndGroupFeature -> CIChatFeatureView(cItem, c.groupFeature, c.preference.enable.iconColor) is CIContent.SndGroupFeature -> CIChatFeatureView(cItem, c.groupFeature, c.preference.enable.iconColor)
is CIContent.RcvChatFeatureRejected -> CIChatFeatureView(cItem, c.feature, Color.Red) is CIContent.RcvChatFeatureRejected -> CIChatFeatureView(cItem, c.feature, Color.Red)
is CIContent.RcvGroupFeatureRejected -> CIChatFeatureView(cItem, c.groupFeature, Color.Red) is CIContent.RcvGroupFeatureRejected -> CIChatFeatureView(cItem, c.groupFeature, Color.Red)
is CIContent.SndModerated -> MarkedDeletedItemView(cItem, cInfo.timedMessagesTTL, showMember = showMember)
is CIContent.RcvModerated -> MarkedDeletedItemView(cItem, cInfo.timedMessagesTTL, showMember = showMember)
is CIContent.InvalidJSON -> CIInvalidJSONView(c.json) is CIContent.InvalidJSON -> CIInvalidJSONView(c.json)
} }
} }
} }
} }
@Composable
fun CancelFileItemAction(
fileId: Long,
showMenu: MutableState<Boolean>,
cancelFile: (Long) -> Unit,
cancelAction: CancelAction
) {
ItemAction(
stringResource(cancelAction.uiActionId),
painterResource(R.drawable.ic_close),
onClick = {
showMenu.value = false
cancelFileAlertDialog(fileId, cancelFile = cancelFile, cancelAction = cancelAction)
},
color = Color.Red
)
}
@Composable @Composable
fun DeleteItemAction( fun DeleteItemAction(
cItem: ChatItem, cItem: ChatItem,
@@ -253,7 +287,7 @@ fun DeleteItemAction(
) { ) {
ItemAction( ItemAction(
stringResource(R.string.delete_verb), stringResource(R.string.delete_verb),
Icons.Outlined.Delete, painterResource(R.drawable.ic_delete),
onClick = { onClick = {
showMenu.value = false showMenu.value = false
deleteMessageAlertDialog(cItem, questionText, deleteMessage = deleteMessage) deleteMessageAlertDialog(cItem, questionText, deleteMessage = deleteMessage)
@@ -263,22 +297,75 @@ fun DeleteItemAction(
} }
@Composable @Composable
fun ItemAction(text: String, icon: ImageVector, onClick: () -> Unit, color: Color = MaterialTheme.colors.onBackground) { fun ModerateItemAction(
DropdownMenuItem(onClick) { cItem: ChatItem,
Row { questionText: String,
showMenu: MutableState<Boolean>,
deleteMessage: (Long, CIDeleteMode) -> Unit
) {
ItemAction(
stringResource(R.string.moderate_verb),
painterResource(R.drawable.ic_flag),
onClick = {
showMenu.value = false
moderateMessageAlertDialog(cItem, questionText, deleteMessage = deleteMessage)
},
color = Color.Red
)
}
@Composable
fun ItemAction(text: String, icon: Painter, onClick: () -> Unit, color: Color = Color.Unspecified) {
val finalColor = if (color == Color.Unspecified) {
if (isInDarkTheme()) MenuTextColorDark else Color.Black
} else color
DropdownMenuItem(onClick, contentPadding = PaddingValues(horizontal = DEFAULT_PADDING * 1.5f)) {
Row(verticalAlignment = Alignment.CenterVertically) {
Text( Text(
text, text,
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.weight(1F) .weight(1F)
.padding(end = 15.dp), .padding(end = 15.dp),
color = color color = finalColor
) )
Icon(icon, text, tint = color) Icon(icon, text, tint = finalColor)
} }
} }
} }
@Composable
fun ItemAction(text: String, icon: ImageVector, onClick: () -> Unit, color: Color = Color.Unspecified) {
val finalColor = if (color == Color.Unspecified) {
if (isInDarkTheme()) MenuTextColorDark else Color.Black
} else color
DropdownMenuItem(onClick, contentPadding = PaddingValues(horizontal = DEFAULT_PADDING * 1.5f)) {
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
text,
modifier = Modifier
.fillMaxWidth()
.weight(1F)
.padding(end = 15.dp),
color = finalColor
)
Icon(icon, text, tint = finalColor)
}
}
}
fun cancelFileAlertDialog(fileId: Long, cancelFile: (Long) -> Unit, cancelAction: CancelAction) {
AlertManager.shared.showAlertDialog(
title = generalGetString(cancelAction.alert.titleId),
text = generalGetString(cancelAction.alert.messageId),
confirmText = generalGetString(cancelAction.alert.confirmId),
destructive = true,
onConfirm = {
cancelFile(fileId)
}
)
}
fun deleteMessageAlertDialog(chatItem: ChatItem, questionText: String, deleteMessage: (Long, CIDeleteMode) -> Unit) { fun deleteMessageAlertDialog(chatItem: ChatItem, questionText: String, deleteMessage: (Long, CIDeleteMode) -> Unit) {
AlertManager.shared.showAlertDialogButtons( AlertManager.shared.showAlertDialogButtons(
title = generalGetString(R.string.delete_message__question), title = generalGetString(R.string.delete_message__question),
@@ -288,24 +375,36 @@ fun deleteMessageAlertDialog(chatItem: ChatItem, questionText: String, deleteMes
Modifier Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 8.dp, vertical = 2.dp), .padding(horizontal = 8.dp, vertical = 2.dp),
horizontalArrangement = Arrangement.End, horizontalArrangement = Arrangement.Center,
) { ) {
TextButton(onClick = { TextButton(onClick = {
deleteMessage(chatItem.id, CIDeleteMode.cidmInternal) deleteMessage(chatItem.id, CIDeleteMode.cidmInternal)
AlertManager.shared.hideAlert() AlertManager.shared.hideAlert()
}) { Text(stringResource(R.string.for_me_only)) } }) { Text(stringResource(R.string.for_me_only), color = MaterialTheme.colors.error) }
if (chatItem.meta.editable) { if (chatItem.meta.editable) {
Spacer(Modifier.padding(horizontal = 4.dp)) Spacer(Modifier.padding(horizontal = 4.dp))
TextButton(onClick = { TextButton(onClick = {
deleteMessage(chatItem.id, CIDeleteMode.cidmBroadcast) deleteMessage(chatItem.id, CIDeleteMode.cidmBroadcast)
AlertManager.shared.hideAlert() AlertManager.shared.hideAlert()
}) { Text(stringResource(R.string.for_everybody)) } }) { Text(stringResource(R.string.for_everybody), color = MaterialTheme.colors.error) }
} }
} }
} }
) )
} }
fun moderateMessageAlertDialog(chatItem: ChatItem, questionText: String, deleteMessage: (Long, CIDeleteMode) -> Unit) {
AlertManager.shared.showAlertDialog(
title = generalGetString(R.string.delete_member_message__question),
text = questionText,
confirmText = generalGetString(R.string.delete_verb),
destructive = true,
onConfirm = {
deleteMessage(chatItem.id, CIDeleteMode.cidmBroadcast)
}
)
}
private fun showMsgDeliveryErrorAlert(description: String) { private fun showMsgDeliveryErrorAlert(description: String) {
AlertManager.shared.showAlertMsg( AlertManager.shared.showAlertMsg(
title = generalGetString(R.string.message_delivery_error_title), title = generalGetString(R.string.message_delivery_error_title),
@@ -327,6 +426,7 @@ fun PreviewChatItemView() {
composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = true)) }, composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = true)) },
deleteMessage = { _, _ -> }, deleteMessage = { _, _ -> },
receiveFile = {}, receiveFile = {},
cancelFile = {},
joinGroup = {}, joinGroup = {},
acceptCall = { _ -> }, acceptCall = { _ -> },
scrollToItem = {}, scrollToItem = {},
@@ -347,6 +447,7 @@ fun PreviewChatItemViewDeletedContent() {
composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = true)) }, composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = true)) },
deleteMessage = { _, _ -> }, deleteMessage = { _, _ -> },
receiveFile = {}, receiveFile = {},
cancelFile = {},
joinGroup = {}, joinGroup = {},
acceptCall = { _ -> }, acceptCall = { _ -> },
scrollToItem = {}, scrollToItem = {},
@@ -1,11 +1,11 @@
package chat.simplex.app.views.chat.item package chat.simplex.app.views.chat.item
import android.content.res.Configuration import android.content.res.Configuration
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.* import androidx.compose.material.*
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.text.* import androidx.compose.ui.text.*
@@ -14,14 +14,16 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import chat.simplex.app.model.ChatItem import chat.simplex.app.model.ChatItem
import chat.simplex.app.ui.theme.HighOrLowlight import chat.simplex.app.ui.theme.*
import chat.simplex.app.ui.theme.SimpleXTheme
@Composable @Composable
fun DeletedItemView(ci: ChatItem, timedMessagesTTL: Int?, showMember: Boolean = false) { fun DeletedItemView(ci: ChatItem, timedMessagesTTL: Int?, showMember: Boolean = false) {
val sent = ci.chatDir.sent
val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage
val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage
Surface( Surface(
shape = RoundedCornerShape(18.dp), shape = RoundedCornerShape(18.dp),
color = ReceivedColorLight, color = if (sent) sentColor else receivedColor,
) { ) {
Row( Row(
Modifier.padding(horizontal = 12.dp, vertical = 6.dp), Modifier.padding(horizontal = 12.dp, vertical = 6.dp),
@@ -30,7 +32,7 @@ fun DeletedItemView(ci: ChatItem, timedMessagesTTL: Int?, showMember: Boolean =
Text( Text(
buildAnnotatedString { buildAnnotatedString {
appendSender(this, if (showMember) ci.memberDisplayName else null, true) appendSender(this, if (showMember) ci.memberDisplayName else null, true)
withStyle(SpanStyle(fontStyle = FontStyle.Italic, color = HighOrLowlight)) { append(ci.content.text) } withStyle(SpanStyle(fontStyle = FontStyle.Italic, color = MaterialTheme.colors.secondary)) { append(ci.content.text) }
}, },
style = MaterialTheme.typography.body1.copy(lineHeight = 22.sp), style = MaterialTheme.typography.body1.copy(lineHeight = 22.sp),
modifier = Modifier.padding(end = 8.dp) modifier = Modifier.padding(end = 8.dp)
@@ -4,9 +4,6 @@ import androidx.compose.foundation.*
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.* import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material.icons.outlined.Delete
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@@ -14,12 +11,14 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.clipToBounds import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.layout.* import androidx.compose.ui.layout.*
import androidx.compose.ui.platform.UriHandler import androidx.compose.ui.platform.UriHandler
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.* import androidx.compose.ui.text.*
import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.* import androidx.compose.ui.tooling.preview.*
import androidx.compose.ui.unit.* import androidx.compose.ui.unit.*
import androidx.compose.ui.util.fastMap import androidx.compose.ui.util.fastMap
@@ -28,11 +27,7 @@ import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.* import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.helpers.* import chat.simplex.app.views.helpers.*
import kotlinx.datetime.Clock import kotlinx.datetime.Clock
import kotlin.math.min
val SentColorLight = Color(0x1E45B8FF)
val ReceivedColorLight = Color(0x20B1B0B5)
val SentQuoteColorLight = Color(0x2545B8FF)
val ReceivedQuoteColorLight = Color(0x25B1B0B5)
@Composable @Composable
fun FramedItemView( fun FramedItemView(
@@ -54,6 +49,9 @@ fun FramedItemView(
return if (chatInfo is ChatInfo.Group) chatInfo.groupInfo.membership else null return if (chatInfo is ChatInfo.Group) chatInfo.groupInfo.membership else null
} }
@Composable
fun Color.toQuote(): Color = if (isInDarkTheme()) lighter(0.12f) else darker(0.12f)
@Composable @Composable
fun ciQuotedMsgView(qi: CIQuote) { fun ciQuotedMsgView(qi: CIQuote) {
Box( Box(
@@ -69,15 +67,14 @@ fun FramedItemView(
} }
@Composable @Composable
fun FramedItemHeader(caption: String, italic: Boolean, icon: ImageVector? = null) { fun FramedItemHeader(caption: String, italic: Boolean, icon: Painter? = null) {
val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage
val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage
Row( Row(
Modifier Modifier
.background(if (sent) SentQuoteColorLight else ReceivedQuoteColorLight) .background(if (sent) sentColor.toQuote() else receivedColor.toQuote())
.fillMaxWidth() .fillMaxWidth()
.padding(start = 8.dp) .padding(start = 8.dp, top = 6.dp, end = 12.dp, bottom = if (ci.quotedItem == null) 6.dp else 0.dp),
.padding(end = 12.dp)
.padding(top = 6.dp)
.padding(bottom = if (ci.quotedItem == null) 6.dp else 0.dp),
horizontalArrangement = Arrangement.spacedBy(4.dp), horizontalArrangement = Arrangement.spacedBy(4.dp),
) { ) {
if (icon != null) { if (icon != null) {
@@ -90,20 +87,24 @@ fun FramedItemView(
} }
Text( Text(
buildAnnotatedString { buildAnnotatedString {
withStyle(SpanStyle(fontSize = 12.sp, fontStyle = if (italic) FontStyle.Italic else FontStyle.Normal, color = HighOrLowlight)) { withStyle(SpanStyle(fontSize = 12.sp, fontStyle = if (italic) FontStyle.Italic else FontStyle.Normal, color = MaterialTheme.colors.secondary)) {
append(caption) append(caption)
} }
}, },
style = MaterialTheme.typography.body1.copy(lineHeight = 22.sp), style = MaterialTheme.typography.body1.copy(lineHeight = 22.sp),
maxLines = 1,
overflow = TextOverflow.Ellipsis
) )
} }
} }
@Composable @Composable
fun ciQuoteView(qi: CIQuote) { fun ciQuoteView(qi: CIQuote) {
val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage
val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage
Row( Row(
Modifier Modifier
.background(if (sent) SentQuoteColorLight else ReceivedQuoteColorLight) .background(if (sent) sentColor.toQuote() else receivedColor.toQuote())
.fillMaxWidth() .fillMaxWidth()
.combinedClickable( .combinedClickable(
onLongClick = { showMenu.value = true }, onLongClick = { showMenu.value = true },
@@ -123,12 +124,24 @@ fun FramedItemView(
modifier = Modifier.size(68.dp).clipToBounds() modifier = Modifier.size(68.dp).clipToBounds()
) )
} }
is MsgContent.MCVideo -> {
Box(Modifier.fillMaxWidth().weight(1f)) {
ciQuotedMsgView(qi)
}
val imageBitmap = base64ToBitmap(qi.content.image).asImageBitmap()
Image(
imageBitmap,
contentDescription = stringResource(R.string.video_descr),
contentScale = ContentScale.Crop,
modifier = Modifier.size(68.dp).clipToBounds()
)
}
is MsgContent.MCFile, is MsgContent.MCVoice -> { is MsgContent.MCFile, is MsgContent.MCVoice -> {
Box(Modifier.fillMaxWidth().weight(1f)) { Box(Modifier.fillMaxWidth().weight(1f)) {
ciQuotedMsgView(qi) ciQuotedMsgView(qi)
} }
Icon( Icon(
if (qi.content is MsgContent.MCFile) Icons.Filled.InsertDriveFile else Icons.Filled.Mic, if (qi.content is MsgContent.MCFile) painterResource(R.drawable.ic_draft_filled) else painterResource(R.drawable.ic_mic_filled),
if (qi.content is MsgContent.MCFile) stringResource(R.string.icon_descr_file) else stringResource(R.string.voice_message), if (qi.content is MsgContent.MCFile) stringResource(R.string.icon_descr_file) else stringResource(R.string.voice_message),
Modifier Modifier
.padding(top = 6.dp, end = 4.dp) .padding(top = 6.dp, end = 4.dp)
@@ -149,23 +162,30 @@ fun FramedItemView(
} }
} }
val transparentBackground = (ci.content.msgContent is MsgContent.MCImage) && !ci.meta.isLive && ci.content.text.isEmpty() && ci.quotedItem == null val transparentBackground = (ci.content.msgContent is MsgContent.MCImage || ci.content.msgContent is MsgContent.MCVideo) &&
!ci.meta.isLive && ci.content.text.isEmpty() && ci.quotedItem == null
val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage
val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage
Box(Modifier Box(Modifier
.clip(RoundedCornerShape(18.dp)) .clip(RoundedCornerShape(18.dp))
.background( .background(
when { when {
transparentBackground -> Color.Transparent transparentBackground -> Color.Transparent
sent -> SentColorLight sent -> sentColor
else -> ReceivedColorLight else -> receivedColor
} }
)) { )) {
var metaColor = HighOrLowlight var metaColor = MaterialTheme.colors.secondary
Box(contentAlignment = Alignment.BottomEnd) { Box(contentAlignment = Alignment.BottomEnd) {
Column(Modifier.width(IntrinsicSize.Max)) { Column(Modifier.width(IntrinsicSize.Max)) {
PriorityLayout(Modifier, CHAT_IMAGE_LAYOUT_ID) { PriorityLayout(Modifier, CHAT_IMAGE_LAYOUT_ID) {
if (ci.meta.itemDeleted) { if (ci.meta.itemDeleted != null) {
FramedItemHeader(stringResource(R.string.marked_deleted_description), true, Icons.Outlined.Delete) if (ci.meta.itemDeleted is CIDeleted.Moderated) {
FramedItemHeader(String.format(stringResource(R.string.moderated_item_description), ci.meta.itemDeleted.byGroupMember.chatViewName), true, painterResource(R.drawable.ic_flag))
} else {
FramedItemHeader(stringResource(R.string.marked_deleted_description), true, painterResource(R.drawable.ic_delete))
}
} else if (ci.meta.isLive) { } else if (ci.meta.isLive) {
FramedItemHeader(stringResource(R.string.live), false) FramedItemHeader(stringResource(R.string.live), false)
} }
@@ -192,6 +212,14 @@ fun FramedItemView(
CIMarkdownText(ci, chatTTL, showMember, linkMode, uriHandler) CIMarkdownText(ci, chatTTL, showMember, linkMode, uriHandler)
} }
} }
is MsgContent.MCVideo -> {
CIVideoView(image = mc.image, mc.duration, file = ci.file, imageProvider ?: return@PriorityLayout, showMenu, receiveFile)
if (mc.text == "" && !ci.meta.isLive) {
metaColor = Color.White
} else {
CIMarkdownText(ci, chatTTL, showMember, linkMode, uriHandler)
}
}
is MsgContent.MCVoice -> { is MsgContent.MCVoice -> {
CIVoiceView(mc.duration, ci.file, ci.meta.itemEdited, ci.chatDir.sent, hasText = true, ci, timedMessagesTTL = chatTTL, longClick = { onLinkLongClick("") }) CIVoiceView(mc.duration, ci.file, ci.meta.itemEdited, ci.chatDir.sent, hasText = true, ci, timedMessagesTTL = chatTTL, longClick = { onLinkLongClick("") })
if (mc.text != "") { if (mc.text != "") {
@@ -241,6 +269,12 @@ fun CIMarkdownText(
} }
const val CHAT_IMAGE_LAYOUT_ID = "chatImage" const val CHAT_IMAGE_LAYOUT_ID = "chatImage"
/**
* Equal to [androidx.compose.ui.unit.Constraints.MaxFocusMask], which is 0x3FFFF - 1
* Other values make a crash `java.lang.IllegalArgumentException: Can't represent a width of 123456 and height of 9909 in Constraints`
* See [androidx.compose.ui.unit.Constraints.createConstraints]
* */
const val MAX_SAFE_WIDTH = 0x3FFFF - 1
@Composable @Composable
fun PriorityLayout( fun PriorityLayout(
@@ -248,6 +282,17 @@ fun PriorityLayout(
priorityLayoutId: String, priorityLayoutId: String,
content: @Composable () -> Unit content: @Composable () -> Unit
) { ) {
/**
* Limiting max value for height + width in order to not crash the app, see [androidx.compose.ui.unit.Constraints.createConstraints]
* */
fun maxSafeHeight(width: Int) = when { // width bits + height bits should be <= 31
width < 0x1FFF /*MaxNonFocusMask*/ -> 0x3FFFF - 1 /* MaxFocusMask */ // 13 bits width + 18 bits height
width < 0x7FFF /*MinNonFocusMask*/ -> 0xFFFF - 1 /* MinFocusMask */ // 15 bits width + 16 bits height
width < 0xFFFF /*MinFocusMask*/ -> 0x7FFF - 1 /* MinFocusMask */ // 16 bits width + 15 bits height
width < 0x3FFFF /*MaxFocusMask*/ -> 0x1FFF - 1 /* MaxNonFocusMask */ // 18 bits width + 13 bits height
else -> 0x1FFF // shouldn't happen since width is limited already
}
Layout( Layout(
content = content, content = content,
modifier = modifier modifier = modifier
@@ -259,9 +304,11 @@ fun PriorityLayout(
if (it.layoutId == priorityLayoutId) if (it.layoutId == priorityLayoutId)
imagePlaceable!! imagePlaceable!!
else else
it.measure(constraints.copy(maxWidth = imagePlaceable?.width ?: constraints.maxWidth)) } it.measure(constraints.copy(maxWidth = imagePlaceable?.width ?: min(MAX_SAFE_WIDTH, constraints.maxWidth))) }
// Limit width for every other element to width of important element and height for a sum of all elements // Limit width for every other element to width of important element and height for a sum of all elements.
layout(imagePlaceable?.measuredWidth ?: placeables.maxOf { it.width }, placeables.sumOf { it.height }) { val width = imagePlaceable?.measuredWidth ?: min(MAX_SAFE_WIDTH, placeables.maxOf { it.width })
val height = minOf(maxSafeHeight(width), placeables.sumOf { it.height })
layout(width, height) {
var y = 0 var y = 0
placeables.forEach { placeables.forEach {
it.place(0, y) it.place(0, y)
@@ -3,21 +3,28 @@ package chat.simplex.app.views.chat.item
import android.graphics.Bitmap import android.graphics.Bitmap
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.view.View
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.compose.foundation.* import androidx.compose.foundation.*
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.* import androidx.compose.ui.graphics.*
import androidx.compose.ui.graphics.painter.BitmapPainter import androidx.compose.ui.graphics.painter.BitmapPainter
import androidx.compose.ui.input.pointer.* import androidx.compose.ui.input.pointer.*
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.*
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.*
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.view.isVisible
import chat.simplex.app.R import chat.simplex.app.R
import chat.simplex.app.views.chat.ProviderMedia
import chat.simplex.app.views.helpers.* import chat.simplex.app.views.helpers.*
import coil.ImageLoader import coil.ImageLoader
import coil.compose.rememberAsyncImagePainter import coil.compose.rememberAsyncImagePainter
@@ -26,13 +33,16 @@ import coil.decode.ImageDecoderDecoder
import coil.request.ImageRequest import coil.request.ImageRequest
import coil.size.Size import coil.size.Size
import com.google.accompanist.pager.* import com.google.accompanist.pager.*
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout
import com.google.android.exoplayer2.ui.StyledPlayerView
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlin.math.absoluteValue import kotlin.math.absoluteValue
interface ImageGalleryProvider { interface ImageGalleryProvider {
val initialIndex: Int val initialIndex: Int
val totalImagesSize: MutableState<Int> val totalMediaSize: MutableState<Int>
fun getImage(index: Int): Pair<Bitmap, Uri>? fun getMedia(index: Int): ProviderMedia?
fun currentPageChanged(index: Int) fun currentPageChanged(index: Int)
fun scrollToStart() fun scrollToStart()
fun onDismiss(index: Int) fun onDismiss(index: Int)
@@ -48,13 +58,17 @@ fun ImageFullScreenView(imageProvider: () -> ImageGalleryProvider, close: () ->
// Pager doesn't ask previous page at initialization step who knows why. By not doing this, prev page is not checked and can be blank, // Pager doesn't ask previous page at initialization step who knows why. By not doing this, prev page is not checked and can be blank,
// which makes this blank page visible for a moment. Prevent it by doing the check ourselves // which makes this blank page visible for a moment. Prevent it by doing the check ourselves
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
if (provider.getImage(provider.initialIndex - 1) == null) { if (provider.getMedia(provider.initialIndex - 1) == null) {
provider.scrollToStart() provider.scrollToStart()
pagerState.scrollToPage(0) pagerState.scrollToPage(0)
} }
} }
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
HorizontalPager(count = remember { provider.totalImagesSize }.value, state = pagerState) { index -> val playersToRelease = rememberSaveable { mutableSetOf<Uri>() }
DisposableEffectOnGone(
whenGone = { playersToRelease.forEach { VideoPlayer.release(it, true, true) } }
)
HorizontalPager(count = remember { provider.totalMediaSize }.value, state = pagerState) { index ->
Column( Column(
Modifier Modifier
.fillMaxSize() .fillMaxSize()
@@ -74,13 +88,13 @@ fun ImageFullScreenView(imageProvider: () -> ImageGalleryProvider, close: () ->
if (settledCurrentPage != provider.initialIndex) if (settledCurrentPage != provider.initialIndex)
provider.currentPageChanged(index) provider.currentPageChanged(index)
} }
val image = provider.getImage(index) val media = provider.getMedia(index)
if (image == null) { if (media == null) {
// No such image. Let's shrink total pages size or scroll to start of the list of pages to remove blank page automatically // No such image. Let's shrink total pages size or scroll to start of the list of pages to remove blank page automatically
SideEffect { SideEffect {
scope.launch { scope.launch {
when (settledCurrentPage) { when (settledCurrentPage) {
index - 1 -> provider.totalImagesSize.value = settledCurrentPage + 1 index - 1 -> provider.totalMediaSize.value = settledCurrentPage + 1
index + 1 -> { index + 1 -> {
provider.scrollToStart() provider.scrollToStart()
pagerState.scrollToPage(0) pagerState.scrollToPage(0)
@@ -89,7 +103,6 @@ fun ImageFullScreenView(imageProvider: () -> ImageGalleryProvider, close: () ->
} }
} }
} else { } else {
val (imageBitmap: Bitmap, uri: Uri) = image
var scale by remember { mutableStateOf(1f) } var scale by remember { mutableStateOf(1f) }
var translationX by remember { mutableStateOf(0f) } var translationX by remember { mutableStateOf(0f) }
var translationY by remember { mutableStateOf(0f) } var translationY by remember { mutableStateOf(0f) }
@@ -100,54 +113,106 @@ fun ImageFullScreenView(imageProvider: () -> ImageGalleryProvider, close: () ->
translationX = 0f translationX = 0f
translationY = 0f translationY = 0f
} }
// I'm making a new instance of imageLoader here because if I use one instance in multiple places val modifier = Modifier
// after end of composition here a GIF from the first instance will be paused automatically which isn't what I want .onGloballyPositioned {
val imageLoader = ImageLoader.Builder(LocalContext.current) viewWidth = it.size.width
.components {
if (Build.VERSION.SDK_INT >= 28) {
add(ImageDecoderDecoder.Factory())
} else {
add(GifDecoder.Factory())
}
} }
.build() .graphicsLayer(
Image( scaleX = scale,
rememberAsyncImagePainter( scaleY = scale,
ImageRequest.Builder(LocalContext.current).data(data = uri).size(Size.ORIGINAL).build(), translationX = translationX,
placeholder = BitmapPainter(imageBitmap.asImageBitmap()), // show original image while it's still loading by coil translationY = translationY,
imageLoader = imageLoader )
), .pointerInput(Unit) {
contentDescription = stringResource(R.string.image_descr), detectTransformGestures(
contentScale = ContentScale.Fit, { allowTranslate },
modifier = Modifier onGesture = { _, pan, gestureZoom, _ ->
.onGloballyPositioned { scale = (scale * gestureZoom).coerceIn(1f, 20f)
viewWidth = it.size.width allowTranslate = viewWidth * (scale - 1f) - ((translationX + pan.x * scale).absoluteValue * 2) > 0
} if (scale > 1 && allowTranslate) {
.graphicsLayer( translationX += pan.x * scale
scaleX = scale, translationY += pan.y * scale
scaleY = scale, } else if (allowTranslate) {
translationX = translationX, translationX = 0f
translationY = translationY, translationY = 0f
)
.pointerInput(Unit) {
detectTransformGestures(
{ allowTranslate },
onGesture = { _, pan, gestureZoom, _ ->
scale = (scale * gestureZoom).coerceIn(1f, 20f)
allowTranslate = viewWidth * (scale - 1f) - ((translationX + pan.x * scale).absoluteValue * 2) > 0
if (scale > 1 && allowTranslate) {
translationX += pan.x * scale
translationY += pan.y * scale
} else if (allowTranslate) {
translationX = 0f
translationY = 0f
}
} }
) }
)
}
.fillMaxSize()
if (media is ProviderMedia.Image) {
val (uri: Uri, imageBitmap: Bitmap) = media
// I'm making a new instance of imageLoader here because if I use one instance in multiple places
// after end of composition here a GIF from the first instance will be paused automatically which isn't what I want
val imageLoader = ImageLoader.Builder(LocalContext.current)
.components {
if (Build.VERSION.SDK_INT >= 28) {
add(ImageDecoderDecoder.Factory())
} else {
add(GifDecoder.Factory())
}
} }
.fillMaxSize(), .build()
) Image(
rememberAsyncImagePainter(
ImageRequest.Builder(LocalContext.current).data(data = uri).size(Size.ORIGINAL).build(),
placeholder = BitmapPainter(imageBitmap.asImageBitmap()), // show original image while it's still loading by coil
imageLoader = imageLoader
),
contentDescription = stringResource(R.string.image_descr),
contentScale = ContentScale.Fit,
modifier = modifier,
)
} else if (media is ProviderMedia.Video) {
val preview = remember(media.uri.path) { base64ToBitmap(media.preview) }
VideoView(modifier, media.uri, preview, index == settledCurrentPage)
DisposableEffect(Unit) {
onDispose { playersToRelease.add(media.uri) }
}
}
} }
} }
} }
} }
@Composable
private fun VideoView(modifier: Modifier, uri: Uri, defaultPreview: Bitmap, currentPage: Boolean) {
val context = LocalContext.current
val player = remember(uri) { VideoPlayer.getOrCreate(uri, true, defaultPreview, 0L, true, context) }
val isCurrentPage = rememberUpdatedState(currentPage)
val play = {
player.play(true)
}
val stop = {
player.stop()
}
LaunchedEffect(Unit) {
player.enableSound(true)
snapshotFlow { isCurrentPage.value }
.distinctUntilChanged()
.collect { if (it) play() else stop() }
}
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
AndroidView(
factory = { ctx ->
StyledPlayerView(ctx).apply {
resizeMode = if (ctx.resources.configuration.screenWidthDp > ctx.resources.configuration.screenHeightDp) {
AspectRatioFrameLayout.RESIZE_MODE_FIXED_HEIGHT
} else {
AspectRatioFrameLayout.RESIZE_MODE_FIXED_WIDTH
}
setShowPreviousButton(false)
setShowNextButton(false)
setShowSubtitleButton(false)
setShowVrButton(false)
controllerAutoShow = false
findViewById<View>(com.google.android.exoplayer2.R.id.exo_controls_background).setBackgroundColor(Color.Black.copy(alpha = 0.3f).toArgb())
findViewById<View>(com.google.android.exoplayer2.R.id.exo_settings).isVisible = false
this.player = player.player
}
},
modifier
)
}
}
@@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.* import androidx.compose.material.*
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
@@ -17,21 +18,45 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import chat.simplex.app.R import chat.simplex.app.R
import chat.simplex.app.model.ChatItem import chat.simplex.app.model.ChatItem
import chat.simplex.app.model.MsgErrorType
import chat.simplex.app.ui.theme.CurrentColors
import chat.simplex.app.ui.theme.SimpleXTheme import chat.simplex.app.ui.theme.SimpleXTheme
import chat.simplex.app.views.helpers.AlertManager import chat.simplex.app.views.helpers.AlertManager
import chat.simplex.app.views.helpers.generalGetString import chat.simplex.app.views.helpers.generalGetString
@Composable @Composable
fun IntegrityErrorItemView(ci: ChatItem, timedMessagesTTL: Int?, showMember: Boolean = false) { fun IntegrityErrorItemView(msgError: MsgErrorType, ci: ChatItem, timedMessagesTTL: Int?, showMember: Boolean = false) {
CIMsgError(ci, timedMessagesTTL, showMember) {
when (msgError) {
is MsgErrorType.MsgSkipped ->
AlertManager.shared.showAlertMsg(
title = generalGetString(R.string.alert_title_skipped_messages),
text = generalGetString(R.string.alert_text_skipped_messages_it_can_happen_when)
)
is MsgErrorType.MsgBadHash ->
AlertManager.shared.showAlertMsg(
title = generalGetString(R.string.alert_title_msg_bad_hash),
text = generalGetString(R.string.alert_text_msg_bad_hash) + "\n" +
generalGetString(R.string.alert_text_fragment_encryption_out_of_sync_old_database) + "\n" +
generalGetString(R.string.alert_text_fragment_please_report_to_developers)
)
is MsgErrorType.MsgBadId, is MsgErrorType.MsgDuplicate ->
AlertManager.shared.showAlertMsg(
title = generalGetString(R.string.alert_title_msg_bad_id),
text = generalGetString(R.string.alert_text_msg_bad_id) + "\n" +
generalGetString(R.string.alert_text_fragment_please_report_to_developers)
)
}
}
}
@Composable
fun CIMsgError(ci: ChatItem, timedMessagesTTL: Int?, showMember: Boolean = false, onClick: () -> Unit) {
val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage
Surface( Surface(
Modifier.clickable(onClick = { Modifier.clickable(onClick = onClick),
AlertManager.shared.showAlertMsg(
title = generalGetString(R.string.alert_title_skipped_messages),
text = generalGetString(R.string.alert_text_skipped_messages_it_can_happen_when)
)
}),
shape = RoundedCornerShape(18.dp), shape = RoundedCornerShape(18.dp),
color = ReceivedColorLight, color = receivedColor,
) { ) {
Row( Row(
Modifier.padding(horizontal = 12.dp, vertical = 6.dp), Modifier.padding(horizontal = 12.dp, vertical = 6.dp),
@@ -59,6 +84,7 @@ fun IntegrityErrorItemView(ci: ChatItem, timedMessagesTTL: Int?, showMember: Boo
fun IntegrityErrorItemViewPreview() { fun IntegrityErrorItemViewPreview() {
SimpleXTheme { SimpleXTheme {
IntegrityErrorItemView( IntegrityErrorItemView(
MsgErrorType.MsgBadHash(),
ChatItem.getDeletedContentSampleData(), ChatItem.getDeletedContentSampleData(),
null null
) )
@@ -1,47 +1,63 @@
package chat.simplex.app.views.chat.item package chat.simplex.app.views.chat.item
import android.content.res.Configuration import android.content.res.Configuration
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.* import androidx.compose.material.*
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.text.* import androidx.compose.ui.text.*
import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import chat.simplex.app.R import chat.simplex.app.R
import chat.simplex.app.model.CIDeleted
import chat.simplex.app.model.ChatItem import chat.simplex.app.model.ChatItem
import chat.simplex.app.ui.theme.HighOrLowlight import chat.simplex.app.ui.theme.*
import chat.simplex.app.ui.theme.SimpleXTheme
import chat.simplex.app.views.helpers.generalGetString import chat.simplex.app.views.helpers.generalGetString
@Composable @Composable
fun MarkedDeletedItemView(ci: ChatItem, timedMessagesTTL: Int?, showMember: Boolean = false) { fun MarkedDeletedItemView(ci: ChatItem, timedMessagesTTL: Int?, showMember: Boolean = false) {
val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage
val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage
Surface( Surface(
shape = RoundedCornerShape(18.dp), shape = RoundedCornerShape(18.dp),
color = if (ci.chatDir.sent) SentColorLight else ReceivedColorLight, color = if (ci.chatDir.sent) sentColor else receivedColor,
) { ) {
Row( Row(
Modifier.padding(horizontal = 12.dp, vertical = 6.dp), Modifier.padding(horizontal = 12.dp, vertical = 6.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Text( Box(Modifier.weight(1f, false)) {
buildAnnotatedString { if (ci.meta.itemDeleted is CIDeleted.Moderated) {
// appendSender(this, if (showMember) ci.memberDisplayName else null, true) // TODO font size MarkedDeletedText(String.format(generalGetString(R.string.moderated_item_description), ci.meta.itemDeleted.byGroupMember.chatViewName))
withStyle(SpanStyle(fontSize = 12.sp, fontStyle = FontStyle.Italic, color = HighOrLowlight)) { append(generalGetString(R.string.marked_deleted_description)) } } else {
}, MarkedDeletedText(generalGetString(R.string.marked_deleted_description))
style = MaterialTheme.typography.body1.copy(lineHeight = 22.sp), }
modifier = Modifier.padding(end = 8.dp) }
)
CIMetaView(ci, timedMessagesTTL) CIMetaView(ci, timedMessagesTTL)
} }
} }
} }
@Composable
private fun MarkedDeletedText(text: String) {
Text(
buildAnnotatedString {
// appendSender(this, if (showMember) ci.memberDisplayName else null, true) // TODO font size
withStyle(SpanStyle(fontSize = 12.sp, fontStyle = FontStyle.Italic, color = MaterialTheme.colors.secondary)) { append(text) }
},
style = MaterialTheme.typography.body1.copy(lineHeight = 22.sp),
modifier = Modifier.padding(end = 8.dp),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
@Preview(showBackground = true) @Preview(showBackground = true)
@Preview( @Preview(
uiMode = Configuration.UI_MODE_NIGHT_YES, uiMode = Configuration.UI_MODE_NIGHT_YES,
@@ -51,7 +67,7 @@ fun MarkedDeletedItemView(ci: ChatItem, timedMessagesTTL: Int?, showMember: Bool
fun PreviewMarkedDeletedItemView() { fun PreviewMarkedDeletedItemView() {
SimpleXTheme { SimpleXTheme {
DeletedItemView( DeletedItemView(
ChatItem.getSampleData(itemDeleted = true), ChatItem.getSampleData(itemDeleted = CIDeleted.Deleted()),
null null
) )
} }
@@ -22,7 +22,7 @@ import androidx.compose.ui.unit.*
import androidx.core.text.BidiFormatter import androidx.core.text.BidiFormatter
import chat.simplex.app.TAG import chat.simplex.app.TAG
import chat.simplex.app.model.* import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.HighOrLowlight import chat.simplex.app.ui.theme.CurrentColors
import chat.simplex.app.views.helpers.detectGesture import chat.simplex.app.views.helpers.detectGesture
import kotlinx.coroutines.* import kotlinx.coroutines.*
@@ -57,7 +57,7 @@ private val typingIndicators: List<AnnotatedString> = listOf(
private fun typingIndicator(recent: Boolean, @IntRange (from = 0, to = 4) typingIdx: Int): AnnotatedString = buildAnnotatedString { private fun typingIndicator(recent: Boolean, @IntRange (from = 0, to = 4) typingIdx: Int): AnnotatedString = buildAnnotatedString {
pushStyle(SpanStyle(color = HighOrLowlight, fontFamily = FontFamily.Monospace, letterSpacing = (-1).sp)) pushStyle(SpanStyle(color = CurrentColors.value.colors.secondary, fontFamily = FontFamily.Monospace, letterSpacing = (-1).sp))
append(if (recent) typingIndicators[typingIdx] else noTyping) append(if (recent) typingIndicators[typingIdx] else noTyping)
} }
@@ -66,7 +66,7 @@ private fun typing(w: FontWeight = FontWeight.Light): AnnotatedString =
@Composable @Composable
fun MarkdownText ( fun MarkdownText (
text: String, text: CharSequence,
formattedText: List<FormattedText>? = null, formattedText: List<FormattedText>? = null,
sender: String? = null, sender: String? = null,
meta: CIMeta? = null, meta: CIMeta? = null,
@@ -78,6 +78,7 @@ fun MarkdownText (
senderBold: Boolean = false, senderBold: Boolean = false,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
linkMode: SimplexLinkMode, linkMode: SimplexLinkMode,
inlineContent: Map<String, InlineTextContent>? = null,
onLinkLongClick: (link: String) -> Unit = {} onLinkLongClick: (link: String) -> Unit = {}
) { ) {
val textLayoutDirection = remember (text) { val textLayoutDirection = remember (text) {
@@ -132,13 +133,14 @@ fun MarkdownText (
if (formattedText == null) { if (formattedText == null) {
val annotatedText = buildAnnotatedString { val annotatedText = buildAnnotatedString {
appendSender(this, sender, senderBold) appendSender(this, sender, senderBold)
append(text) if (text is String) append(text)
else if (text is AnnotatedString) append(text)
if (meta?.isLive == true) { if (meta?.isLive == true) {
append(typingIndicator(meta.recent, typingIdx)) append(typingIndicator(meta.recent, typingIdx))
} }
if (meta != null) withStyle(reserveTimestampStyle) { append(reserve) } if (meta != null) withStyle(reserveTimestampStyle) { append(reserve) }
} }
Text(annotatedText, style = style, modifier = modifier, maxLines = maxLines, overflow = overflow) Text(annotatedText, style = style, modifier = modifier, maxLines = maxLines, overflow = overflow, inlineContent = inlineContent ?: mapOf())
} else { } else {
var hasLinks = false var hasLinks = false
val annotatedText = buildAnnotatedString { val annotatedText = buildAnnotatedString {
@@ -226,7 +228,7 @@ fun ClickableText(
} }
} }
}, shouldConsumeEvent = { pos -> }, shouldConsumeEvent = { pos ->
var consume = false var consume = false
layoutResult.value?.let { layoutResult -> layoutResult.value?.let { layoutResult ->
consume = shouldConsumeEvent(layoutResult.getOffsetForPosition(pos)) consume = shouldConsumeEvent(layoutResult.getOffsetForPosition(pos))
} }
@@ -4,8 +4,7 @@ import android.content.res.Configuration
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.material.* import androidx.compose.material.*
import androidx.compose.material.icons.Icons import androidx.compose.ui.res.painterResource
import androidx.compose.material.icons.outlined.PersonAdd
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@@ -19,6 +18,7 @@ import androidx.compose.ui.unit.sp
import chat.simplex.app.R import chat.simplex.app.R
import chat.simplex.app.ui.theme.SimpleXTheme import chat.simplex.app.ui.theme.SimpleXTheme
import chat.simplex.app.views.helpers.annotatedStringResource import chat.simplex.app.views.helpers.annotatedStringResource
import chat.simplex.app.views.helpers.openUriCatching
import chat.simplex.app.views.usersettings.MarkdownHelpView import chat.simplex.app.views.usersettings.MarkdownHelpView
import chat.simplex.app.views.usersettings.simplexTeamUri import chat.simplex.app.views.usersettings.simplexTeamUri
@@ -27,7 +27,6 @@ val bold = SpanStyle(fontWeight = FontWeight.Bold)
@Composable @Composable
fun ChatHelpView(addContact: (() -> Unit)? = null) { fun ChatHelpView(addContact: (() -> Unit)? = null) {
Column( Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(10.dp) verticalArrangement = Arrangement.spacedBy(10.dp)
) { ) {
val uriHandler = LocalUriHandler.current val uriHandler = LocalUriHandler.current
@@ -36,14 +35,13 @@ fun ChatHelpView(addContact: (() -> Unit)? = null) {
Text( Text(
annotatedStringResource(R.string.you_can_connect_to_simplex_chat_founder), annotatedStringResource(R.string.you_can_connect_to_simplex_chat_founder),
modifier = Modifier.clickable(onClick = { modifier = Modifier.clickable(onClick = {
uriHandler.openUri(simplexTeamUri) uriHandler.openUriCatching(simplexTeamUri)
}), }),
lineHeight = 22.sp lineHeight = 22.sp
) )
Column( Column(
Modifier.padding(top = 24.dp), Modifier.padding(top = 24.dp),
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(10.dp) verticalArrangement = Arrangement.spacedBy(10.dp)
) { ) {
Text( Text(
@@ -57,7 +55,7 @@ fun ChatHelpView(addContact: (() -> Unit)? = null) {
) { ) {
Text(stringResource(R.string.chat_help_tap_button)) Text(stringResource(R.string.chat_help_tap_button))
Icon( Icon(
Icons.Outlined.PersonAdd, painterResource(R.drawable.ic_person_add),
stringResource(R.string.add_contact), stringResource(R.string.add_contact),
modifier = if (addContact != null) Modifier.clickable(onClick = addContact) else Modifier, modifier = if (addContact != null) Modifier.clickable(onClick = addContact) else Modifier,
) )
@@ -69,7 +67,6 @@ fun ChatHelpView(addContact: (() -> Unit)? = null) {
Column( Column(
Modifier.padding(top = 24.dp), Modifier.padding(top = 24.dp),
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(10.dp) verticalArrangement = Arrangement.spacedBy(10.dp)
) { ) {
Text(stringResource(R.string.to_connect_via_link_title), style = MaterialTheme.typography.h2) Text(stringResource(R.string.to_connect_via_link_title), style = MaterialTheme.typography.h2)
@@ -80,7 +77,6 @@ fun ChatHelpView(addContact: (() -> Unit)? = null) {
Column( Column(
Modifier.padding(vertical = 24.dp), Modifier.padding(vertical = 24.dp),
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(10.dp) verticalArrangement = Arrangement.spacedBy(10.dp)
) { ) {
Text(stringResource(R.string.markdown_in_messages), style = MaterialTheme.typography.h2) Text(stringResource(R.string.markdown_in_messages), style = MaterialTheme.typography.h2)
@@ -4,14 +4,12 @@ import android.content.res.Configuration
import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.material.* import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material.icons.outlined.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
@@ -44,17 +42,19 @@ fun ChatListNavLinkView(chat: Chat, chatModel: ChatModel) {
delay(500L) delay(500L)
} }
when (chat.chatInfo) { when (chat.chatInfo) {
is ChatInfo.Direct -> is ChatInfo.Direct -> {
val contactNetworkStatus = chatModel.contactNetworkStatus(chat.chatInfo.contact)
ChatListNavLinkLayout( ChatListNavLinkLayout(
chatLinkPreview = { ChatPreviewView(chat, chatModel.incognito.value, chatModel.currentUser.value?.profile?.displayName, stopped, linkMode) }, chatLinkPreview = { ChatPreviewView(chat, chatModel.draft.value, chatModel.draftChatId.value, chatModel.incognito.value, chatModel.currentUser.value?.profile?.displayName, contactNetworkStatus, stopped, linkMode) },
click = { directChatAction(chat.chatInfo, chatModel) }, click = { directChatAction(chat.chatInfo, chatModel) },
dropdownMenuItems = { ContactMenuItems(chat, chatModel, showMenu, showMarkRead) }, dropdownMenuItems = { ContactMenuItems(chat, chatModel, showMenu, showMarkRead) },
showMenu, showMenu,
stopped stopped
) )
}
is ChatInfo.Group -> is ChatInfo.Group ->
ChatListNavLinkLayout( ChatListNavLinkLayout(
chatLinkPreview = { ChatPreviewView(chat, chatModel.incognito.value, chatModel.currentUser.value?.profile?.displayName, stopped, linkMode) }, chatLinkPreview = { ChatPreviewView(chat, chatModel.draft.value, chatModel.draftChatId.value, chatModel.incognito.value, chatModel.currentUser.value?.profile?.displayName, null, stopped, linkMode) },
click = { groupChatAction(chat.chatInfo.groupInfo, chatModel) }, click = { groupChatAction(chat.chatInfo.groupInfo, chatModel) },
dropdownMenuItems = { GroupMenuItems(chat, chat.chatInfo.groupInfo, chatModel, showMenu, showMarkRead) }, dropdownMenuItems = { GroupMenuItems(chat, chat.chatInfo.groupInfo, chatModel, showMenu, showMarkRead) },
showMenu, showMenu,
@@ -181,7 +181,7 @@ fun GroupMenuItems(chat: Chat, groupInfo: GroupInfo, chatModel: ChatModel, showM
fun MarkReadChatAction(chat: Chat, chatModel: ChatModel, showMenu: MutableState<Boolean>) { fun MarkReadChatAction(chat: Chat, chatModel: ChatModel, showMenu: MutableState<Boolean>) {
ItemAction( ItemAction(
stringResource(R.string.mark_read), stringResource(R.string.mark_read),
Icons.Outlined.Check, painterResource(R.drawable.ic_check),
onClick = { onClick = {
markChatRead(chat, chatModel) markChatRead(chat, chatModel)
chatModel.controller.ntfManager.cancelNotificationsForChat(chat.id) chatModel.controller.ntfManager.cancelNotificationsForChat(chat.id)
@@ -192,33 +192,21 @@ fun MarkReadChatAction(chat: Chat, chatModel: ChatModel, showMenu: MutableState<
@Composable @Composable
fun MarkUnreadChatAction(chat: Chat, chatModel: ChatModel, showMenu: MutableState<Boolean>) { fun MarkUnreadChatAction(chat: Chat, chatModel: ChatModel, showMenu: MutableState<Boolean>) {
DropdownMenuItem({ ItemAction(
markChatUnread(chat, chatModel) stringResource(R.string.mark_unread),
showMenu.value = false painterResource(R.drawable.ic_mark_chat_unread),
}) { onClick = {
Row { markChatUnread(chat, chatModel)
Text( showMenu.value = false
stringResource(R.string.mark_unread),
modifier = Modifier
.fillMaxWidth()
.weight(1F)
.padding(end = 15.dp),
color = MaterialTheme.colors.onBackground
)
Icon(
Icons.Outlined.MarkChatUnread,
stringResource(R.string.mark_unread),
tint = MaterialTheme.colors.onBackground
)
} }
} )
} }
@Composable @Composable
fun ToggleNotificationsChatAction(chat: Chat, chatModel: ChatModel, ntfsEnabled: Boolean, showMenu: MutableState<Boolean>) { fun ToggleNotificationsChatAction(chat: Chat, chatModel: ChatModel, ntfsEnabled: Boolean, showMenu: MutableState<Boolean>) {
ItemAction( ItemAction(
if (ntfsEnabled) stringResource(R.string.mute_chat) else stringResource(R.string.unmute_chat), if (ntfsEnabled) stringResource(R.string.mute_chat) else stringResource(R.string.unmute_chat),
if (ntfsEnabled) Icons.Outlined.NotificationsOff else Icons.Outlined.Notifications, if (ntfsEnabled) painterResource(R.drawable.ic_notifications_off) else painterResource(R.drawable.ic_notifications),
onClick = { onClick = {
changeNtfsStatePerChat(!ntfsEnabled, mutableStateOf(ntfsEnabled), chat, chatModel) changeNtfsStatePerChat(!ntfsEnabled, mutableStateOf(ntfsEnabled), chat, chatModel)
showMenu.value = false showMenu.value = false
@@ -230,7 +218,7 @@ fun ToggleNotificationsChatAction(chat: Chat, chatModel: ChatModel, ntfsEnabled:
fun ClearChatAction(chat: Chat, chatModel: ChatModel, showMenu: MutableState<Boolean>) { fun ClearChatAction(chat: Chat, chatModel: ChatModel, showMenu: MutableState<Boolean>) {
ItemAction( ItemAction(
stringResource(R.string.clear_chat_menu_action), stringResource(R.string.clear_chat_menu_action),
Icons.Outlined.Restore, painterResource(R.drawable.ic_settings_backup_restore),
onClick = { onClick = {
clearChatDialog(chat.chatInfo, chatModel) clearChatDialog(chat.chatInfo, chatModel)
showMenu.value = false showMenu.value = false
@@ -243,7 +231,7 @@ fun ClearChatAction(chat: Chat, chatModel: ChatModel, showMenu: MutableState<Boo
fun DeleteContactAction(chat: Chat, chatModel: ChatModel, showMenu: MutableState<Boolean>) { fun DeleteContactAction(chat: Chat, chatModel: ChatModel, showMenu: MutableState<Boolean>) {
ItemAction( ItemAction(
stringResource(R.string.delete_contact_menu_action), stringResource(R.string.delete_contact_menu_action),
Icons.Outlined.Delete, painterResource(R.drawable.ic_delete),
onClick = { onClick = {
deleteContactDialog(chat.chatInfo, chatModel) deleteContactDialog(chat.chatInfo, chatModel)
showMenu.value = false showMenu.value = false
@@ -256,7 +244,7 @@ fun DeleteContactAction(chat: Chat, chatModel: ChatModel, showMenu: MutableState
fun DeleteGroupAction(chat: Chat, groupInfo: GroupInfo, chatModel: ChatModel, showMenu: MutableState<Boolean>) { fun DeleteGroupAction(chat: Chat, groupInfo: GroupInfo, chatModel: ChatModel, showMenu: MutableState<Boolean>) {
ItemAction( ItemAction(
stringResource(R.string.delete_group_menu_action), stringResource(R.string.delete_group_menu_action),
Icons.Outlined.Delete, painterResource(R.drawable.ic_delete),
onClick = { onClick = {
deleteGroupDialog(chat.chatInfo, groupInfo, chatModel) deleteGroupDialog(chat.chatInfo, groupInfo, chatModel)
showMenu.value = false showMenu.value = false
@@ -270,7 +258,7 @@ fun JoinGroupAction(chat: Chat, groupInfo: GroupInfo, chatModel: ChatModel, show
val joinGroup: () -> Unit = { withApi { chatModel.controller.apiJoinGroup(groupInfo.groupId) } } val joinGroup: () -> Unit = { withApi { chatModel.controller.apiJoinGroup(groupInfo.groupId) } }
ItemAction( ItemAction(
if (chat.chatInfo.incognito) stringResource(R.string.join_group_incognito_button) else stringResource(R.string.join_group_button), if (chat.chatInfo.incognito) stringResource(R.string.join_group_incognito_button) else stringResource(R.string.join_group_button),
if (chat.chatInfo.incognito) Icons.Filled.TheaterComedy else Icons.Outlined.Login, if (chat.chatInfo.incognito) painterResource(R.drawable.ic_theater_comedy_filled) else painterResource(R.drawable.ic_login),
color = if (chat.chatInfo.incognito) Indigo else MaterialTheme.colors.onBackground, color = if (chat.chatInfo.incognito) Indigo else MaterialTheme.colors.onBackground,
onClick = { onClick = {
joinGroup() joinGroup()
@@ -283,7 +271,7 @@ fun JoinGroupAction(chat: Chat, groupInfo: GroupInfo, chatModel: ChatModel, show
fun LeaveGroupAction(groupInfo: GroupInfo, chatModel: ChatModel, showMenu: MutableState<Boolean>) { fun LeaveGroupAction(groupInfo: GroupInfo, chatModel: ChatModel, showMenu: MutableState<Boolean>) {
ItemAction( ItemAction(
stringResource(R.string.leave_group_button), stringResource(R.string.leave_group_button),
Icons.Outlined.Logout, painterResource(R.drawable.ic_logout),
onClick = { onClick = {
leaveGroupDialog(groupInfo, chatModel) leaveGroupDialog(groupInfo, chatModel)
showMenu.value = false showMenu.value = false
@@ -296,16 +284,16 @@ fun LeaveGroupAction(groupInfo: GroupInfo, chatModel: ChatModel, showMenu: Mutab
fun ContactRequestMenuItems(chatInfo: ChatInfo.ContactRequest, chatModel: ChatModel, showMenu: MutableState<Boolean>) { fun ContactRequestMenuItems(chatInfo: ChatInfo.ContactRequest, chatModel: ChatModel, showMenu: MutableState<Boolean>) {
ItemAction( ItemAction(
if (chatModel.incognito.value) stringResource(R.string.accept_contact_incognito_button) else stringResource(R.string.accept_contact_button), if (chatModel.incognito.value) stringResource(R.string.accept_contact_incognito_button) else stringResource(R.string.accept_contact_button),
if (chatModel.incognito.value) Icons.Filled.TheaterComedy else Icons.Outlined.Check, if (chatModel.incognito.value) painterResource(R.drawable.ic_theater_comedy_filled) else painterResource(R.drawable.ic_check),
color = if (chatModel.incognito.value) Indigo else MaterialTheme.colors.onBackground, color = if (chatModel.incognito.value) Indigo else MaterialTheme.colors.onBackground,
onClick = { onClick = {
acceptContactRequest(chatInfo, chatModel) acceptContactRequest(chatInfo.apiId, chatInfo, true, chatModel)
showMenu.value = false showMenu.value = false
} }
) )
ItemAction( ItemAction(
stringResource(R.string.reject_contact_button), stringResource(R.string.reject_contact_button),
Icons.Outlined.Close, painterResource(R.drawable.ic_close),
onClick = { onClick = {
rejectContactRequest(chatInfo, chatModel) rejectContactRequest(chatInfo, chatModel)
showMenu.value = false showMenu.value = false
@@ -318,7 +306,7 @@ fun ContactRequestMenuItems(chatInfo: ChatInfo.ContactRequest, chatModel: ChatMo
fun ContactConnectionMenuItems(chatInfo: ChatInfo.ContactConnection, chatModel: ChatModel, showMenu: MutableState<Boolean>) { fun ContactConnectionMenuItems(chatInfo: ChatInfo.ContactConnection, chatModel: ChatModel, showMenu: MutableState<Boolean>) {
ItemAction( ItemAction(
stringResource(R.string.set_contact_name), stringResource(R.string.set_contact_name),
Icons.Outlined.Edit, painterResource(R.drawable.ic_edit),
onClick = { onClick = {
ModalManager.shared.showModalCloseable(true) { close -> ModalManager.shared.showModalCloseable(true) { close ->
ContactConnectionInfoView(chatModel, chatInfo.contactConnection.connReqInv, chatInfo.contactConnection, true, close) ContactConnectionInfoView(chatModel, chatInfo.contactConnection.connReqInv, chatInfo.contactConnection, true, close)
@@ -328,7 +316,7 @@ fun ContactConnectionMenuItems(chatInfo: ChatInfo.ContactConnection, chatModel:
) )
ItemAction( ItemAction(
stringResource(R.string.delete_verb), stringResource(R.string.delete_verb),
Icons.Outlined.Delete, painterResource(R.drawable.ic_delete),
onClick = { onClick = {
deleteContactConnectionAlert(chatInfo.contactConnection, chatModel) {} deleteContactConnectionAlert(chatInfo.contactConnection, chatModel) {}
showMenu.value = false showMenu.value = false
@@ -340,7 +328,7 @@ fun ContactConnectionMenuItems(chatInfo: ChatInfo.ContactConnection, chatModel:
@Composable @Composable
private fun InvalidDataView() { private fun InvalidDataView() {
Row { Row {
ProfileImage(72.dp, null, Icons.Filled.AccountCircle, HighOrLowlight) ProfileImage(72.dp, null, R.drawable.ic_account_circle_filled, MaterialTheme.colors.secondary)
Column( Column(
modifier = Modifier modifier = Modifier
.padding(horizontal = 8.dp) .padding(horizontal = 8.dp)
@@ -407,16 +395,16 @@ fun contactRequestAlertDialog(contactRequest: ChatInfo.ContactRequest, chatModel
title = generalGetString(R.string.accept_connection_request__question), title = generalGetString(R.string.accept_connection_request__question),
text = generalGetString(R.string.if_you_choose_to_reject_the_sender_will_not_be_notified), text = generalGetString(R.string.if_you_choose_to_reject_the_sender_will_not_be_notified),
confirmText = if (chatModel.incognito.value) generalGetString(R.string.accept_contact_incognito_button) else generalGetString(R.string.accept_contact_button), confirmText = if (chatModel.incognito.value) generalGetString(R.string.accept_contact_incognito_button) else generalGetString(R.string.accept_contact_button),
onConfirm = { acceptContactRequest(contactRequest, chatModel) }, onConfirm = { acceptContactRequest(contactRequest.apiId, contactRequest, true, chatModel) },
dismissText = generalGetString(R.string.reject_contact_button), dismissText = generalGetString(R.string.reject_contact_button),
onDismiss = { rejectContactRequest(contactRequest, chatModel) } onDismiss = { rejectContactRequest(contactRequest, chatModel) }
) )
} }
fun acceptContactRequest(contactRequest: ChatInfo.ContactRequest, chatModel: ChatModel) { fun acceptContactRequest(apiId: Long, contactRequest: ChatInfo.ContactRequest?, isCurrentUser: Boolean, chatModel: ChatModel) {
withApi { withApi {
val contact = chatModel.controller.apiAcceptContactRequest(contactRequest.apiId) val contact = chatModel.controller.apiAcceptContactRequest(apiId)
if (contact != null) { if (contact != null && isCurrentUser && contactRequest != null) {
val chat = Chat(ChatInfo.Direct(contact), listOf()) val chat = Chat(ChatInfo.Direct(contact), listOf())
chatModel.replaceChat(contactRequest.id, chat) chatModel.replaceChat(contactRequest.id, chat)
} }
@@ -445,7 +433,7 @@ fun contactConnectionAlertDialog(connection: PendingContactConnection, chatModel
Modifier Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 8.dp, vertical = 2.dp), .padding(horizontal = 8.dp, vertical = 2.dp),
horizontalArrangement = Arrangement.End, horizontalArrangement = Arrangement.Center,
) { ) {
TextButton(onClick = { TextButton(onClick = {
AlertManager.shared.hideAlert() AlertManager.shared.hideAlert()
@@ -478,7 +466,8 @@ fun deleteContactConnectionAlert(connection: PendingContactConnection, chatModel
onSuccess() onSuccess()
} }
} }
} },
destructive = true,
) )
} }
@@ -496,6 +485,7 @@ fun pendingContactAlertDialog(chatInfo: ChatInfo, chatModel: ChatModel) {
} }
} }
}, },
destructive = true,
dismissText = generalGetString(R.string.cancel_verb), dismissText = generalGetString(R.string.cancel_verb),
) )
} }
@@ -587,15 +577,7 @@ fun ChatListNavLinkLayout(
chatLinkPreview() chatLinkPreview()
} }
if (dropdownMenuItems != null) { if (dropdownMenuItems != null) {
Box(Modifier.padding(horizontal = 16.dp)) { DefaultDropdownMenu(showMenu, dropdownMenuItems = dropdownMenuItems)
DropdownMenu(
expanded = showMenu.value,
onDismissRequest = { showMenu.value = false },
Modifier.width(220.dp)
) {
dropdownMenuItems()
}
}
} }
} }
Divider(Modifier.padding(horizontal = 8.dp)) Divider(Modifier.padding(horizontal = 8.dp))
@@ -625,8 +607,11 @@ fun PreviewChatListNavLinkDirect() {
), ),
chatStats = Chat.ChatStats() chatStats = Chat.ChatStats()
), ),
null,
null,
false, false,
null, null,
null,
stopped = false, stopped = false,
linkMode = SimplexLinkMode.DESCRIPTION linkMode = SimplexLinkMode.DESCRIPTION
) )
@@ -663,8 +648,11 @@ fun PreviewChatListNavLinkGroup() {
), ),
chatStats = Chat.ChatStats() chatStats = Chat.ChatStats()
), ),
null,
null,
false, false,
null, null,
null,
stopped = false, stopped = false,
linkMode = SimplexLinkMode.DESCRIPTION linkMode = SimplexLinkMode.DESCRIPTION
) )
@@ -4,25 +4,24 @@ import androidx.activity.compose.BackHandler
import androidx.compose.foundation.* import androidx.compose.foundation.*
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.* import androidx.compose.foundation.lazy.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.* import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material.icons.outlined.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.*
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.capitalize import androidx.compose.ui.text.capitalize
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.intl.Locale import androidx.compose.ui.text.intl.Locale
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.*
import androidx.fragment.app.FragmentActivity
import chat.simplex.app.*
import chat.simplex.app.R import chat.simplex.app.R
import chat.simplex.app.connectIfOpenedViaUri
import chat.simplex.app.model.* import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.* import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.helpers.* import chat.simplex.app.views.helpers.*
@@ -36,14 +35,15 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@Composable @Composable
fun ChatListView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit, stopped: Boolean) { fun ChatListView(chatModel: ChatModel, setPerformLA: (Boolean, FragmentActivity) -> Unit, stopped: Boolean) {
val newChatSheetState by rememberSaveable(stateSaver = NewChatSheetState.saver()) { mutableStateOf(MutableStateFlow(NewChatSheetState.GONE)) } val newChatSheetState by rememberSaveable(stateSaver = AnimatedViewState.saver()) { mutableStateOf(MutableStateFlow(AnimatedViewState.GONE)) }
val userPickerState by rememberSaveable(stateSaver = AnimatedViewState.saver()) { mutableStateOf(MutableStateFlow(AnimatedViewState.GONE)) }
val showNewChatSheet = { val showNewChatSheet = {
newChatSheetState.value = NewChatSheetState.VISIBLE newChatSheetState.value = AnimatedViewState.VISIBLE
} }
val hideNewChatSheet: (animated: Boolean) -> Unit = { animated -> val hideNewChatSheet: (animated: Boolean) -> Unit = { animated ->
if (animated) newChatSheetState.value = NewChatSheetState.HIDING if (animated) newChatSheetState.value = AnimatedViewState.HIDING
else newChatSheetState.value = NewChatSheetState.GONE else newChatSheetState.value = AnimatedViewState.GONE
} }
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
if (shouldShowWhatsNew(chatModel)) { if (shouldShowWhatsNew(chatModel)) {
@@ -63,10 +63,12 @@ fun ChatListView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit, stopped:
} }
var searchInList by rememberSaveable { mutableStateOf("") } var searchInList by rememberSaveable { mutableStateOf("") }
val scaffoldState = rememberScaffoldState() val scaffoldState = rememberScaffoldState()
Scaffold( val scope = rememberCoroutineScope()
topBar = { ChatListToolbar(chatModel, scaffoldState.drawerState, stopped) { searchInList = it.trim() } }, val switchingUsers = rememberSaveable { mutableStateOf(false) }
Scaffold(topBar = { ChatListToolbar(chatModel, scaffoldState.drawerState, userPickerState, stopped) { searchInList = it.trim() } },
scaffoldState = scaffoldState, scaffoldState = scaffoldState,
drawerContent = { SettingsView(chatModel, setPerformLA) }, drawerContent = { SettingsView(chatModel, setPerformLA) },
drawerScrimColor = MaterialTheme.colors.onSurface.copy(alpha = if (isInDarkTheme()) 0.16f else 0.32f),
floatingActionButton = { floatingActionButton = {
if (searchInList.isEmpty()) { if (searchInList.isEmpty()) {
FloatingActionButton( FloatingActionButton(
@@ -75,16 +77,17 @@ fun ChatListView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit, stopped:
if (newChatSheetState.value.isVisible()) hideNewChatSheet(true) else showNewChatSheet() if (newChatSheetState.value.isVisible()) hideNewChatSheet(true) else showNewChatSheet()
} }
}, },
Modifier.padding(end = DEFAULT_PADDING - 16.dp, bottom = DEFAULT_PADDING - 16.dp),
elevation = FloatingActionButtonDefaults.elevation( elevation = FloatingActionButtonDefaults.elevation(
defaultElevation = 0.dp, defaultElevation = 0.dp,
pressedElevation = 0.dp, pressedElevation = 0.dp,
hoveredElevation = 0.dp, hoveredElevation = 0.dp,
focusedElevation = 0.dp, focusedElevation = 0.dp,
), ),
backgroundColor = if (!stopped) MaterialTheme.colors.primary else HighOrLowlight, backgroundColor = if (!stopped) MaterialTheme.colors.primary else MaterialTheme.colors.secondary,
contentColor = Color.White contentColor = Color.White
) { ) {
Icon(if (!newChatSheetState.collectAsState().value.isVisible()) Icons.Default.Edit else Icons.Default.Close, stringResource(R.string.add_contact_or_create_group)) Icon(if (!newChatSheetState.collectAsState().value.isVisible()) painterResource(R.drawable.ic_edit_filled) else painterResource(R.drawable.ic_close), stringResource(R.string.add_contact_or_create_group))
} }
} }
} }
@@ -93,16 +96,15 @@ fun ChatListView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit, stopped:
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.background(MaterialTheme.colors.background)
) { ) {
if (chatModel.chats.isNotEmpty()) { if (chatModel.chats.isNotEmpty()) {
ChatList(chatModel, search = searchInList) ChatList(chatModel, search = searchInList)
} else { } else if (!switchingUsers.value) {
Box(Modifier.fillMaxSize()) { Box(Modifier.fillMaxSize()) {
if (!stopped && !newChatSheetState.collectAsState().value.isVisible()) { if (!stopped && !newChatSheetState.collectAsState().value.isVisible()) {
OnboardingButtons(showNewChatSheet) OnboardingButtons(showNewChatSheet)
} }
Text(stringResource(R.string.you_have_no_chats), Modifier.align(Alignment.Center), color = HighOrLowlight) Text(stringResource(R.string.you_have_no_chats), Modifier.align(Alignment.Center), color = MaterialTheme.colors.secondary)
} }
} }
} }
@@ -111,6 +113,17 @@ fun ChatListView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit, stopped:
if (searchInList.isEmpty()) { if (searchInList.isEmpty()) {
NewChatSheet(chatModel, newChatSheetState, stopped, hideNewChatSheet) NewChatSheet(chatModel, newChatSheetState, stopped, hideNewChatSheet)
} }
UserPicker(chatModel, userPickerState, switchingUsers) {
scope.launch { if (scaffoldState.drawerState.isOpen) scaffoldState.drawerState.close() else scaffoldState.drawerState.open() }
}
if (switchingUsers.value) {
Box(
Modifier.fillMaxSize().clickable(enabled = false, onClick = {}),
contentAlignment = Alignment.Center
) {
ProgressIndicator()
}
}
} }
@Composable @Composable
@@ -118,11 +131,11 @@ private fun OnboardingButtons(openNewChatSheet: () -> Unit) {
Column(Modifier.fillMaxSize().padding(DEFAULT_PADDING), horizontalAlignment = Alignment.End, verticalArrangement = Arrangement.Bottom) { Column(Modifier.fillMaxSize().padding(DEFAULT_PADDING), horizontalAlignment = Alignment.End, verticalArrangement = Arrangement.Bottom) {
val uriHandler = LocalUriHandler.current val uriHandler = LocalUriHandler.current
ConnectButton(generalGetString(R.string.chat_with_developers)) { ConnectButton(generalGetString(R.string.chat_with_developers)) {
uriHandler.openUri(simplexTeamUri) uriHandler.openUriCatching(simplexTeamUri)
} }
Spacer(Modifier.height(DEFAULT_PADDING)) Spacer(Modifier.height(DEFAULT_PADDING))
ConnectButton(generalGetString(R.string.tap_to_start_new_chat), openNewChatSheet) ConnectButton(generalGetString(R.string.tap_to_start_new_chat), openNewChatSheet)
val color = MaterialTheme.colors.primary val color = MaterialTheme.colors.primaryVariant
Canvas(modifier = Modifier.width(40.dp).height(10.dp), onDraw = { Canvas(modifier = Modifier.width(40.dp).height(10.dp), onDraw = {
val trianglePath = Path().apply { val trianglePath = Path().apply {
moveTo(0.dp.toPx(), 0f) moveTo(0.dp.toPx(), 0f)
@@ -145,7 +158,7 @@ private fun ConnectButton(text: String, onClick: () -> Unit) {
onClick, onClick,
shape = RoundedCornerShape(21.dp), shape = RoundedCornerShape(21.dp),
colors = ButtonDefaults.textButtonColors( colors = ButtonDefaults.textButtonColors(
backgroundColor = MaterialTheme.colors.primary backgroundColor = MaterialTheme.colors.primaryVariant
), ),
elevation = null, elevation = null,
contentPadding = PaddingValues(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF), contentPadding = PaddingValues(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF),
@@ -156,7 +169,7 @@ private fun ConnectButton(text: String, onClick: () -> Unit) {
} }
@Composable @Composable
private fun ChatListToolbar(chatModel: ChatModel, drawerState: DrawerState, stopped: Boolean, onSearchValueChanged: (String) -> Unit) { private fun ChatListToolbar(chatModel: ChatModel, drawerState: DrawerState, userPickerState: MutableStateFlow<AnimatedViewState>, stopped: Boolean, onSearchValueChanged: (String) -> Unit) {
var showSearch by rememberSaveable { mutableStateOf(false) } var showSearch by rememberSaveable { mutableStateOf(false) }
val hideSearchOnBack = { onSearchValueChanged(""); showSearch = false } val hideSearchOnBack = { onSearchValueChanged(""); showSearch = false }
if (showSearch) { if (showSearch) {
@@ -166,7 +179,7 @@ private fun ChatListToolbar(chatModel: ChatModel, drawerState: DrawerState, stop
if (chatModel.chats.size >= 8) { if (chatModel.chats.size >= 8) {
barButtons.add { barButtons.add {
IconButton({ showSearch = true }) { IconButton({ showSearch = true }) {
Icon(Icons.Outlined.Search, stringResource(android.R.string.search_go).capitalize(Locale.current), tint = MaterialTheme.colors.primary) Icon(painterResource(R.drawable.ic_search_500), stringResource(android.R.string.search_go).capitalize(Locale.current), tint = MaterialTheme.colors.primary)
} }
} }
} }
@@ -179,7 +192,7 @@ private fun ChatListToolbar(chatModel: ChatModel, drawerState: DrawerState, stop
) )
}) { }) {
Icon( Icon(
Icons.Filled.Report, painterResource(R.drawable.ic_report_filled),
generalGetString(R.string.chat_is_stopped_indication), generalGetString(R.string.chat_is_stopped_indication),
tint = Color.Red, tint = Color.Red,
) )
@@ -189,10 +202,23 @@ private fun ChatListToolbar(chatModel: ChatModel, drawerState: DrawerState, stop
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
DefaultTopAppBar( DefaultTopAppBar(
navigationButton = { navigationButton = {
if (showSearch) if (showSearch) {
NavigationButtonBack(hideSearchOnBack) NavigationButtonBack(hideSearchOnBack)
else } else if (chatModel.users.isEmpty()) {
NavigationButtonMenu { scope.launch { if (drawerState.isOpen) drawerState.close() else drawerState.open() } } NavigationButtonMenu { scope.launch { if (drawerState.isOpen) drawerState.close() else drawerState.open() } }
} else {
val users by remember { derivedStateOf { chatModel.users.filter { u -> u.user.activeUser || !u.user.hidden } } }
val allRead = users
.filter { u -> !u.user.activeUser && !u.user.hidden }
.all { u -> u.unreadCount == 0 }
UserProfileButton(chatModel.currentUser.value?.profile?.image, allRead) {
if (users.size == 1) {
scope.launch { drawerState.open() }
} else {
userPickerState.value = AnimatedViewState.VISIBLE
}
}
}
}, },
title = { title = {
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
@@ -203,7 +229,7 @@ private fun ChatListToolbar(chatModel: ChatModel, drawerState: DrawerState, stop
) )
if (chatModel.incognito.value) { if (chatModel.incognito.value) {
Icon( Icon(
Icons.Filled.TheaterComedy, painterResource(R.drawable.ic_theater_comedy_filled),
stringResource(R.string.incognito), stringResource(R.string.incognito),
tint = Indigo, tint = Indigo,
modifier = Modifier.padding(10.dp).size(26.dp) modifier = Modifier.padding(10.dp).size(26.dp)
@@ -219,6 +245,47 @@ private fun ChatListToolbar(chatModel: ChatModel, drawerState: DrawerState, stop
Divider(Modifier.padding(top = AppBarHeight)) Divider(Modifier.padding(top = AppBarHeight))
} }
@Composable
fun UserProfileButton(image: String?, allRead: Boolean, onButtonClicked: () -> Unit) {
IconButton(onClick = onButtonClicked) {
Box {
ProfileImage(
image = image,
size = 37.dp
)
if (!allRead) {
unreadBadge()
}
}
}
}
@Composable
private fun BoxScope.unreadBadge(text: String? = "") {
Text(
text ?: "",
color = MaterialTheme.colors.onPrimary,
fontSize = 6.sp,
modifier = Modifier
.background(MaterialTheme.colors.primary, shape = CircleShape)
.badgeLayout()
.padding(horizontal = 3.dp)
.padding(vertical = 1.dp)
.align(Alignment.TopEnd)
)
}
@Composable
private fun ProgressIndicator() {
CircularProgressIndicator(
Modifier
.padding(horizontal = 2.dp)
.size(30.dp),
color = MaterialTheme.colors.secondary,
strokeWidth = 2.5.dp
)
}
private var lazyListState = 0 to 0 private var lazyListState = 0 to 0
@Composable @Composable
@@ -4,16 +4,17 @@ import android.content.res.Configuration
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.text.InlineTextContent
import androidx.compose.foundation.text.appendInlineContent
import androidx.compose.material.* import androidx.compose.material.*
import androidx.compose.material.icons.Icons import androidx.compose.runtime.*
import androidx.compose.material.icons.filled.*
import androidx.compose.material.icons.outlined.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.*
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
@@ -21,20 +22,31 @@ import androidx.compose.ui.unit.*
import chat.simplex.app.R import chat.simplex.app.R
import chat.simplex.app.model.* import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.* import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.chat.ComposePreview
import chat.simplex.app.views.chat.ComposeState
import chat.simplex.app.views.chat.item.MarkdownText import chat.simplex.app.views.chat.item.MarkdownText
import chat.simplex.app.views.helpers.* import chat.simplex.app.views.helpers.*
@Composable @Composable
fun ChatPreviewView(chat: Chat, chatModelIncognito: Boolean, currentUserProfileDisplayName: String?, stopped: Boolean, linkMode: SimplexLinkMode) { fun ChatPreviewView(
chat: Chat,
chatModelDraft: ComposeState?,
chatModelDraftChatId: ChatId?,
chatModelIncognito: Boolean,
currentUserProfileDisplayName: String?,
contactNetworkStatus: NetworkStatus?,
stopped: Boolean,
linkMode: SimplexLinkMode
) {
val cInfo = chat.chatInfo val cInfo = chat.chatInfo
@Composable @Composable
fun groupInactiveIcon() { fun groupInactiveIcon() {
Icon( Icon(
Icons.Filled.Cancel, painterResource(R.drawable.ic_cancel_filled),
stringResource(R.string.icon_descr_group_inactive), stringResource(R.string.icon_descr_group_inactive),
Modifier.size(18.dp).background(MaterialTheme.colors.background, CircleShape), Modifier.size(18.dp).background(MaterialTheme.colors.background, CircleShape),
tint = HighOrLowlight tint = MaterialTheme.colors.secondary
) )
} }
@@ -64,7 +76,44 @@ fun ChatPreviewView(chat: Chat, chatModelIncognito: Boolean, currentUserProfileD
@Composable @Composable
fun VerifiedIcon() { fun VerifiedIcon() {
Icon(Icons.Outlined.VerifiedUser, null, Modifier.size(19.dp).padding(end = 3.dp, top = 1.dp), tint = HighOrLowlight) Icon(painterResource(R.drawable.ic_verified_user), null, Modifier.size(19.dp).padding(end = 3.dp, top = 1.dp), tint = MaterialTheme.colors.secondary)
}
fun messageDraft(draft: ComposeState): Pair<AnnotatedString, Map<String, InlineTextContent>> {
fun attachment(): Pair<Int, String?>? =
when (draft.preview) {
is ComposePreview.FilePreview -> R.drawable.ic_draft_filled to draft.preview.fileName
is ComposePreview.MediaPreview -> R.drawable.ic_image to null
is ComposePreview.VoicePreview -> R.drawable.ic_play_arrow_filled to durationText(draft.preview.durationMs / 1000)
else -> null
}
val attachment = attachment()
val text = buildAnnotatedString {
appendInlineContent(id = "editIcon")
append(" ")
if (attachment != null) {
appendInlineContent(id = "attachmentIcon")
if (attachment.second != null) {
append(attachment.second as String)
}
append(" ")
}
append(draft.message)
}
val inlineContent: Map<String, InlineTextContent> = mapOf(
"editIcon" to InlineTextContent(
Placeholder(20.sp, 20.sp, PlaceholderVerticalAlign.TextCenter)
) {
Icon(painterResource(R.drawable.ic_edit_note), null, tint = MaterialTheme.colors.primary)
},
"attachmentIcon" to InlineTextContent(
Placeholder(20.sp, 20.sp, PlaceholderVerticalAlign.TextCenter)
) {
Icon(if (attachment?.first != null) painterResource(attachment.first) else painterResource(R.drawable.ic_edit_note), null, tint = MaterialTheme.colors.secondary)
}
)
return text to inlineContent
} }
@Composable @Composable
@@ -75,12 +124,12 @@ fun ChatPreviewView(chat: Chat, chatModelIncognito: Boolean, currentUserProfileD
if (cInfo.contact.verified) { if (cInfo.contact.verified) {
VerifiedIcon() VerifiedIcon()
} }
chatPreviewTitleText(if (cInfo.ready) Color.Unspecified else HighOrLowlight) chatPreviewTitleText(if (cInfo.ready) Color.Unspecified else MaterialTheme.colors.secondary)
} }
is ChatInfo.Group -> is ChatInfo.Group ->
when (cInfo.groupInfo.membership.memberStatus) { when (cInfo.groupInfo.membership.memberStatus) {
GroupMemberStatus.MemInvited -> chatPreviewTitleText(if (chat.chatInfo.incognito) Indigo else MaterialTheme.colors.primary) GroupMemberStatus.MemInvited -> chatPreviewTitleText(if (chat.chatInfo.incognito) Indigo else MaterialTheme.colors.primary)
GroupMemberStatus.MemAccepted -> chatPreviewTitleText(HighOrLowlight) GroupMemberStatus.MemAccepted -> chatPreviewTitleText(MaterialTheme.colors.secondary)
else -> chatPreviewTitleText() else -> chatPreviewTitleText()
} }
else -> chatPreviewTitleText() else -> chatPreviewTitleText()
@@ -91,27 +140,42 @@ fun ChatPreviewView(chat: Chat, chatModelIncognito: Boolean, currentUserProfileD
fun chatPreviewText(chatModelIncognito: Boolean) { fun chatPreviewText(chatModelIncognito: Boolean) {
val ci = chat.chatItems.lastOrNull() val ci = chat.chatItems.lastOrNull()
if (ci != null) { if (ci != null) {
val (text: CharSequence, inlineTextContent) = when {
chatModelDraftChatId == chat.id && chatModelDraft != null -> remember(chatModelDraft) { messageDraft(chatModelDraft) }
ci.meta.itemDeleted == null -> ci.text to null
else -> generalGetString(R.string.marked_deleted_description) to null
}
val formattedText = when {
chatModelDraftChatId == chat.id && chatModelDraft != null -> null
ci.meta.itemDeleted == null -> ci.formattedText
else -> null
}
MarkdownText( MarkdownText(
if (!ci.meta.itemDeleted) ci.text else generalGetString(R.string.marked_deleted_description), text,
if (!ci.meta.itemDeleted) ci.formattedText else null, formattedText,
sender = if (cInfo is ChatInfo.Group && !ci.chatDir.sent) ci.memberDisplayName else null, sender = when {
chatModelDraftChatId == chat.id && chatModelDraft != null -> null
cInfo is ChatInfo.Group && !ci.chatDir.sent -> ci.memberDisplayName
else -> null
},
linkMode = linkMode, linkMode = linkMode,
senderBold = true, senderBold = true,
maxLines = 2, maxLines = 2,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.body1.copy(color = if (isInDarkTheme()) MessagePreviewDark else MessagePreviewLight, lineHeight = 22.sp), style = MaterialTheme.typography.body1.copy(color = if (isInDarkTheme()) MessagePreviewDark else MessagePreviewLight, lineHeight = 22.sp),
inlineContent = inlineTextContent,
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
) )
} else { } else {
when (cInfo) { when (cInfo) {
is ChatInfo.Direct -> is ChatInfo.Direct ->
if (!cInfo.ready) { if (!cInfo.ready) {
Text(stringResource(R.string.contact_connection_pending), color = HighOrLowlight) Text(stringResource(R.string.contact_connection_pending), color = MaterialTheme.colors.secondary)
} }
is ChatInfo.Group -> is ChatInfo.Group ->
when (cInfo.groupInfo.membership.memberStatus) { when (cInfo.groupInfo.membership.memberStatus) {
GroupMemberStatus.MemInvited -> Text(groupInvitationPreviewText(chatModelIncognito, currentUserProfileDisplayName, cInfo.groupInfo)) GroupMemberStatus.MemInvited -> Text(groupInvitationPreviewText(chatModelIncognito, currentUserProfileDisplayName, cInfo.groupInfo))
GroupMemberStatus.MemAccepted -> Text(stringResource(R.string.group_connection_pending), color = HighOrLowlight) GroupMemberStatus.MemAccepted -> Text(stringResource(R.string.group_connection_pending), color = MaterialTheme.colors.secondary)
else -> {} else -> {}
} }
else -> {} else -> {}
@@ -144,7 +208,7 @@ fun ChatPreviewView(chat: Chat, chatModelIncognito: Boolean, currentUserProfileD
) { ) {
Text( Text(
ts, ts,
color = HighOrLowlight, color = MaterialTheme.colors.secondary,
style = MaterialTheme.typography.body2, style = MaterialTheme.typography.body2,
modifier = Modifier.padding(bottom = 5.dp) modifier = Modifier.padding(bottom = 5.dp)
) )
@@ -157,10 +221,10 @@ fun ChatPreviewView(chat: Chat, chatModelIncognito: Boolean, currentUserProfileD
) { ) {
Text( Text(
if (n > 0) unreadCountStr(n) else "", if (n > 0) unreadCountStr(n) else "",
color = MaterialTheme.colors.onPrimary, color = Color.White,
fontSize = 11.sp, fontSize = 11.sp,
modifier = Modifier modifier = Modifier
.background(if (stopped || showNtfsIcon) HighOrLowlight else MaterialTheme.colors.primary, shape = CircleShape) .background(if (stopped || showNtfsIcon) MaterialTheme.colors.secondary else MaterialTheme.colors.primaryVariant, shape = CircleShape)
.badgeLayout() .badgeLayout()
.padding(horizontal = 3.dp) .padding(horizontal = 3.dp)
.padding(vertical = 1.dp) .padding(vertical = 1.dp)
@@ -172,9 +236,9 @@ fun ChatPreviewView(chat: Chat, chatModelIncognito: Boolean, currentUserProfileD
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
Icon( Icon(
Icons.Filled.NotificationsOff, painterResource(R.drawable.ic_notifications_off_filled),
contentDescription = generalGetString(R.string.notifications), contentDescription = generalGetString(R.string.notifications),
tint = HighOrLowlight, tint = MaterialTheme.colors.secondary,
modifier = Modifier modifier = Modifier
.padding(horizontal = 3.dp) .padding(horizontal = 3.dp)
.padding(vertical = 1.dp) .padding(vertical = 1.dp)
@@ -187,7 +251,7 @@ fun ChatPreviewView(chat: Chat, chatModelIncognito: Boolean, currentUserProfileD
Modifier.padding(top = 52.dp), Modifier.padding(top = 52.dp),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
ChatStatusImage(chat) ChatStatusImage(contactNetworkStatus)
} }
} }
} }
@@ -210,23 +274,22 @@ fun unreadCountStr(n: Int): String {
} }
@Composable @Composable
fun ChatStatusImage(chat: Chat) { fun ChatStatusImage(s: NetworkStatus?) {
val s = chat.serverInfo.networkStatus val descr = s?.statusString
val descr = s.statusString if (s is NetworkStatus.Error) {
if (s is Chat.NetworkStatus.Error) {
Icon( Icon(
Icons.Outlined.ErrorOutline, painterResource(R.drawable.ic_error),
contentDescription = descr, contentDescription = descr,
tint = HighOrLowlight, tint = MaterialTheme.colors.secondary,
modifier = Modifier modifier = Modifier
.size(19.dp) .size(19.dp)
) )
} else if (s !is Chat.NetworkStatus.Connected) { } else if (s !is NetworkStatus.Connected) {
CircularProgressIndicator( CircularProgressIndicator(
Modifier Modifier
.padding(horizontal = 2.dp) .padding(horizontal = 2.dp)
.size(15.dp), .size(15.dp),
color = HighOrLowlight, color = MaterialTheme.colors.secondary,
strokeWidth = 1.5.dp strokeWidth = 1.5.dp
) )
} }
@@ -241,6 +304,6 @@ fun ChatStatusImage(chat: Chat) {
@Composable @Composable
fun PreviewChatPreviewView() { fun PreviewChatPreviewView() {
SimpleXTheme { SimpleXTheme {
ChatPreviewView(Chat.sampleData, false, "", stopped = false, linkMode = SimplexLinkMode.DESCRIPTION) ChatPreviewView(Chat.sampleData, null, null, false, "", contactNetworkStatus = NetworkStatus.Connected(), stopped = false, linkMode = SimplexLinkMode.DESCRIPTION)
} }
} }
@@ -3,16 +3,16 @@ package chat.simplex.app.views.chatlist
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import chat.simplex.app.R
import chat.simplex.app.model.* import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.* import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.helpers.ProfileImage import chat.simplex.app.views.helpers.ProfileImage
@@ -21,7 +21,7 @@ import chat.simplex.app.views.helpers.ProfileImage
fun ContactConnectionView(contactConnection: PendingContactConnection) { fun ContactConnectionView(contactConnection: PendingContactConnection) {
Row { Row {
Box(Modifier.size(72.dp), contentAlignment = Alignment.Center) { Box(Modifier.size(72.dp), contentAlignment = Alignment.Center) {
ProfileImage(size = 54.dp, null, if (contactConnection.initiated) Icons.Outlined.AddLink else Icons.Outlined.Link) ProfileImage(size = 54.dp, null, if (contactConnection.initiated) R.drawable.ic_add_link else R.drawable.ic_link)
} }
Column( Column(
modifier = Modifier modifier = Modifier
@@ -34,7 +34,7 @@ fun ContactConnectionView(contactConnection: PendingContactConnection) {
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.h3, style = MaterialTheme.typography.h3,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
color = HighOrLowlight color = MaterialTheme.colors.secondary
) )
val height = with(LocalDensity.current) { 46.sp.toDp() } val height = with(LocalDensity.current) { 46.sp.toDp() }
Text(contactConnection.description, Modifier.heightIn(min = height), maxLines = 2, color = if (isInDarkTheme()) MessagePreviewDark else MessagePreviewLight) Text(contactConnection.description, Modifier.heightIn(min = height), maxLines = 2, color = if (isInDarkTheme()) MessagePreviewDark else MessagePreviewLight)
@@ -42,11 +42,10 @@ fun ContactConnectionView(contactConnection: PendingContactConnection) {
val ts = getTimestampText(contactConnection.updatedAt) val ts = getTimestampText(contactConnection.updatedAt)
Column( Column(
Modifier.fillMaxHeight(), Modifier.fillMaxHeight(),
verticalArrangement = Arrangement.Top
) { ) {
Text( Text(
ts, ts,
color = HighOrLowlight, color = MaterialTheme.colors.secondary,
style = MaterialTheme.typography.body2, style = MaterialTheme.typography.body2,
modifier = Modifier.padding(bottom = 5.dp) modifier = Modifier.padding(bottom = 5.dp)
) )
@@ -39,11 +39,10 @@ fun ContactRequestView(chatModelIncognito: Boolean, contactRequest: ChatInfo.Con
val ts = getTimestampText(contactRequest.contactRequest.updatedAt) val ts = getTimestampText(contactRequest.contactRequest.updatedAt)
Column( Column(
Modifier.fillMaxHeight(), Modifier.fillMaxHeight(),
verticalArrangement = Arrangement.Top
) { ) {
Text( Text(
ts, ts,
color = HighOrLowlight, color = MaterialTheme.colors.secondary,
style = MaterialTheme.typography.body2, style = MaterialTheme.typography.body2,
modifier = Modifier.padding(bottom = 5.dp) modifier = Modifier.padding(bottom = 5.dp)
) )
@@ -6,14 +6,12 @@ import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.material.* import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material.icons.outlined.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.capitalize import androidx.compose.ui.text.capitalize
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
@@ -21,21 +19,23 @@ import androidx.compose.ui.text.intl.Locale
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import chat.simplex.app.R import chat.simplex.app.R
import chat.simplex.app.model.* import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.HighOrLowlight import chat.simplex.app.ui.theme.*
import chat.simplex.app.ui.theme.Indigo
import chat.simplex.app.views.helpers.* import chat.simplex.app.views.helpers.*
import kotlinx.coroutines.flow.MutableStateFlow
@Composable @Composable
fun ShareListView(chatModel: ChatModel, stopped: Boolean) { fun ShareListView(chatModel: ChatModel, stopped: Boolean) {
var searchInList by rememberSaveable { mutableStateOf("") } var searchInList by rememberSaveable { mutableStateOf("") }
val userPickerState by rememberSaveable(stateSaver = AnimatedViewState.saver()) { mutableStateOf(MutableStateFlow(AnimatedViewState.GONE)) }
val switchingUsers = rememberSaveable { mutableStateOf(false) }
Scaffold( Scaffold(
topBar = { Column { ShareListToolbar(chatModel, stopped) { searchInList = it.trim() } } }, topBar = { Column { ShareListToolbar(chatModel, userPickerState, stopped) { searchInList = it.trim() } } },
) { ) {
Box(Modifier.padding(it)) { Box(Modifier.padding(it)) {
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.background(MaterialTheme.colors.background) .themedBackground()
) { ) {
if (chatModel.chats.isNotEmpty()) { if (chatModel.chats.isNotEmpty()) {
ShareList(chatModel, search = searchInList) ShareList(chatModel, search = searchInList)
@@ -45,27 +45,45 @@ fun ShareListView(chatModel: ChatModel, stopped: Boolean) {
} }
} }
} }
UserPicker(chatModel, userPickerState, switchingUsers, showSettings = false, showCancel = true, cancelClicked = {
chatModel.sharedContent.value = null
})
} }
@Composable @Composable
private fun EmptyList() { private fun EmptyList() {
Box { Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Text(stringResource(R.string.you_have_no_chats), Modifier.align(Alignment.Center), color = HighOrLowlight) Text(stringResource(R.string.you_have_no_chats), color = MaterialTheme.colors.secondary)
} }
} }
@Composable @Composable
private fun ShareListToolbar(chatModel: ChatModel, stopped: Boolean, onSearchValueChanged: (String) -> Unit) { private fun ShareListToolbar(chatModel: ChatModel, userPickerState: MutableStateFlow<AnimatedViewState>, stopped: Boolean, onSearchValueChanged: (String) -> Unit) {
var showSearch by rememberSaveable { mutableStateOf(false) } var showSearch by rememberSaveable { mutableStateOf(false) }
val hideSearchOnBack = { onSearchValueChanged(""); showSearch = false } val hideSearchOnBack = { onSearchValueChanged(""); showSearch = false }
if (showSearch) { if (showSearch) {
BackHandler(onBack = hideSearchOnBack) BackHandler(onBack = hideSearchOnBack)
} }
val barButtons = arrayListOf<@Composable RowScope.() -> Unit>() val barButtons = arrayListOf<@Composable RowScope.() -> Unit>()
val users by remember { derivedStateOf { chatModel.users.filter { u -> u.user.activeUser || !u.user.hidden } } }
val navButton: @Composable RowScope.() -> Unit = {
when {
showSearch -> NavigationButtonBack(hideSearchOnBack)
users.size > 1 -> {
val allRead = users
.filter { u -> !u.user.activeUser && !u.user.hidden }
.all { u -> u.unreadCount == 0 }
UserProfileButton(chatModel.currentUser.value?.profile?.image, allRead) {
userPickerState.value = AnimatedViewState.VISIBLE
}
}
else -> NavigationButtonBack { chatModel.sharedContent.value = null }
}
}
if (chatModel.chats.size >= 8) { if (chatModel.chats.size >= 8) {
barButtons.add { barButtons.add {
IconButton({ showSearch = true }) { IconButton({ showSearch = true }) {
Icon(Icons.Outlined.Search, stringResource(android.R.string.search_go).capitalize(Locale.current), tint = MaterialTheme.colors.primary) Icon(painterResource(R.drawable.ic_search_500), stringResource(android.R.string.search_go).capitalize(Locale.current), tint = MaterialTheme.colors.primary)
} }
} }
} }
@@ -78,7 +96,7 @@ private fun ShareListToolbar(chatModel: ChatModel, stopped: Boolean, onSearchVal
) )
}) { }) {
Icon( Icon(
Icons.Filled.Report, painterResource(R.drawable.ic_report_filled),
generalGetString(R.string.chat_is_stopped_indication), generalGetString(R.string.chat_is_stopped_indication),
tint = Color.Red, tint = Color.Red,
) )
@@ -87,13 +105,13 @@ private fun ShareListToolbar(chatModel: ChatModel, stopped: Boolean, onSearchVal
} }
DefaultTopAppBar( DefaultTopAppBar(
navigationButton = { if (showSearch) NavigationButtonBack(hideSearchOnBack) else NavigationButtonBack { chatModel.sharedContent.value = null } }, navigationButton = navButton,
title = { title = {
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
Text( Text(
when (chatModel.sharedContent.value) { when (chatModel.sharedContent.value) {
is SharedContent.Text -> stringResource(R.string.share_message) is SharedContent.Text -> stringResource(R.string.share_message)
is SharedContent.Images -> stringResource(R.string.share_image) is SharedContent.Media -> stringResource(R.string.share_image)
is SharedContent.File -> stringResource(R.string.share_file) is SharedContent.File -> stringResource(R.string.share_file)
else -> stringResource(R.string.share_message) else -> stringResource(R.string.share_message)
}, },
@@ -102,7 +120,7 @@ private fun ShareListToolbar(chatModel: ChatModel, stopped: Boolean, onSearchVal
) )
if (chatModel.incognito.value) { if (chatModel.incognito.value) {
Icon( Icon(
Icons.Filled.TheaterComedy, painterResource(R.drawable.ic_theater_comedy_filled),
stringResource(R.string.incognito), stringResource(R.string.incognito),
tint = Indigo, tint = Indigo,
modifier = Modifier.padding(10.dp).size(26.dp) modifier = Modifier.padding(10.dp).size(26.dp)
@@ -0,0 +1,241 @@
package chat.simplex.app.views.chatlist
import SectionItemView
import android.util.Log
import androidx.compose.animation.core.*
import androidx.compose.foundation.*
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.*
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.*
import androidx.compose.ui.draw.*
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.capitalize
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.intl.Locale
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.*
import chat.simplex.app.R
import chat.simplex.app.TAG
import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.helpers.*
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import kotlin.math.roundToInt
@Composable
fun UserPicker(
chatModel: ChatModel,
userPickerState: MutableStateFlow<AnimatedViewState>,
switchingUsers: MutableState<Boolean>,
showSettings: Boolean = true,
showCancel: Boolean = false,
cancelClicked: () -> Unit = {},
settingsClicked: () -> Unit = {},
) {
val scope = rememberCoroutineScope()
var newChat by remember { mutableStateOf(userPickerState.value) }
val users by remember {
derivedStateOf {
chatModel.users
.filter { u -> u.user.activeUser || !u.user.hidden }
.sortedByDescending { it.user.activeUser }
}
}
val animatedFloat = remember { Animatable(if (newChat.isVisible()) 0f else 1f) }
LaunchedEffect(Unit) {
launch {
userPickerState.collect {
newChat = it
launch {
animatedFloat.animateTo(if (newChat.isVisible()) 1f else 0f, newChatSheetAnimSpec())
if (newChat.isHiding()) userPickerState.value = AnimatedViewState.GONE
}
}
}
}
LaunchedEffect(Unit) {
snapshotFlow { newChat.isVisible() }
.distinctUntilChanged()
.filter { it }
.collect {
try {
val updatedUsers = chatModel.controller.listUsers().sortedByDescending { it.user.activeUser }
var same = users.size == updatedUsers.size
if (same) {
for (i in 0 until minOf(users.size, updatedUsers.size)) {
val prev = updatedUsers[i].user
val next = users[i].user
if (prev.userId != next.userId || prev.activeUser != next.activeUser || prev.chatViewName != next.chatViewName || prev.image != next.image) {
same = false
break
}
}
}
if (!same) {
chatModel.users.clear()
chatModel.users.addAll(updatedUsers)
}
} catch (e: Exception) {
Log.e(TAG, "Error updating users ${e.stackTraceToString()}")
}
}
}
val xOffset = with(LocalDensity.current) { 10.dp.roundToPx() }
val maxWidth = with(LocalDensity.current) { LocalConfiguration.current.screenWidthDp * density }
Box(Modifier
.fillMaxSize()
.offset { IntOffset(if (newChat.isGone()) -maxWidth.roundToInt() else xOffset, 0) }
.clickable(interactionSource = remember { MutableInteractionSource() }, indication = null, onClick = { userPickerState.value = AnimatedViewState.HIDING })
.padding(bottom = 10.dp, top = 10.dp)
.graphicsLayer {
alpha = animatedFloat.value
translationY = (animatedFloat.value - 1) * xOffset
}
) {
Column(
Modifier
.widthIn(min = 260.dp)
.width(IntrinsicSize.Min)
.height(IntrinsicSize.Min)
.shadow(8.dp, RoundedCornerShape(corner = CornerSize(25.dp)), clip = true)
.background(MaterialTheme.colors.surface, RoundedCornerShape(corner = CornerSize(25.dp)))
.clip(RoundedCornerShape(corner = CornerSize(25.dp)))
) {
Column(Modifier.weight(1f).verticalScroll(rememberScrollState())) {
users.forEach { u ->
UserProfilePickerItem(u.user, u.unreadCount, PaddingValues(start = DEFAULT_PADDING_HALF, end = DEFAULT_PADDING), openSettings = {
settingsClicked()
userPickerState.value = AnimatedViewState.GONE
}) {
userPickerState.value = AnimatedViewState.HIDING
if (!u.user.activeUser) {
scope.launch {
val job = launch {
delay(500)
switchingUsers.value = true
}
chatModel.controller.changeActiveUser(u.user.userId, null)
job.cancel()
switchingUsers.value = false
}
}
}
Divider(Modifier.requiredHeight(1.dp))
if (u.user.activeUser) Divider(Modifier.requiredHeight(0.5.dp))
}
}
if (showSettings) {
SettingsPickerItem {
settingsClicked()
userPickerState.value = AnimatedViewState.GONE
}
}
if (showCancel) {
CancelPickerItem {
cancelClicked()
userPickerState.value = AnimatedViewState.GONE
}
}
}
}
}
@Composable
fun UserProfilePickerItem(u: User, unreadCount: Int = 0, padding: PaddingValues = PaddingValues(start = DEFAULT_PADDING_HALF, end = DEFAULT_PADDING), onLongClick: () -> Unit = {}, openSettings: () -> Unit = {}, onClick: () -> Unit) {
Row(
Modifier
.fillMaxWidth()
.sizeIn(minHeight = 46.dp)
.combinedClickable(
onClick = if (u.activeUser) openSettings else onClick,
onLongClick = onLongClick,
interactionSource = remember { MutableInteractionSource() },
indication = if (!u.activeUser) LocalIndication.current else null
)
.padding(padding),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
UserProfileRow(u)
if (u.activeUser) {
Icon(painterResource(R.drawable.ic_done_filled), null, Modifier.size(20.dp), tint = MaterialTheme.colors.onBackground)
} else if (u.hidden) {
Icon(painterResource(R.drawable.ic_lock), null, Modifier.size(20.dp), tint = MaterialTheme.colors.secondary)
} else if (unreadCount > 0) {
Box(
contentAlignment = Alignment.Center
) {
Text(
unreadCountStr(unreadCount),
color = Color.White,
fontSize = 11.sp,
modifier = Modifier
.background(MaterialTheme.colors.primaryVariant, shape = CircleShape)
.padding(2.dp)
.badgeLayout()
)
}
} else if (!u.showNtfs) {
Icon(painterResource(R.drawable.ic_notifications_off), null, Modifier.size(20.dp), tint = MaterialTheme.colors.secondary)
} else {
Box(Modifier.size(20.dp))
}
}
}
@Composable
fun UserProfileRow(u: User) {
Row(
Modifier
.widthIn(max = LocalConfiguration.current.screenWidthDp.dp * 0.7f)
.padding(vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
ProfileImage(
image = u.image,
size = 54.dp
)
Text(
u.displayName,
modifier = Modifier
.padding(start = 10.dp, end = 8.dp),
color = if (isInDarkTheme()) MenuTextColorDark else Color.Black,
fontWeight = if (u.activeUser) FontWeight.Medium else FontWeight.Normal
)
}
}
@Composable
private fun SettingsPickerItem(onClick: () -> Unit) {
SectionItemView(onClick, padding = PaddingValues(start = DEFAULT_PADDING + 7.dp, end = DEFAULT_PADDING), minHeight = 68.dp) {
val text = generalGetString(R.string.settings_section_title_settings).lowercase().capitalize(Locale.current)
Icon(painterResource(R.drawable.ic_settings), text, Modifier.size(20.dp), tint = MaterialTheme.colors.onBackground)
Spacer(Modifier.width(DEFAULT_PADDING + 6.dp))
Text(
text,
color = if (isInDarkTheme()) MenuTextColorDark else Color.Black,
)
}
}
@Composable
private fun CancelPickerItem(onClick: () -> Unit) {
SectionItemView(onClick, padding = PaddingValues(start = DEFAULT_PADDING + 7.dp, end = DEFAULT_PADDING), minHeight = 68.dp) {
val text = generalGetString(R.string.cancel_verb)
Icon(painterResource(R.drawable.ic_close), text, Modifier.size(20.dp), tint = MaterialTheme.colors.onBackground)
Spacer(Modifier.width(DEFAULT_PADDING + 6.dp))
Text(
text,
color = if (isInDarkTheme()) MenuTextColorDark else Color.Black,
)
}
}
@@ -1,6 +1,6 @@
package chat.simplex.app.views.database package chat.simplex.app.views.database
import SectionDivider import SectionBottomSpacer
import SectionTextFooter import SectionTextFooter
import SectionView import SectionView
import android.content.Context import android.content.Context
@@ -13,16 +13,14 @@ import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.material.* import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import chat.simplex.app.R import chat.simplex.app.R
import chat.simplex.app.TAG import chat.simplex.app.TAG
import chat.simplex.app.model.ChatModel import chat.simplex.app.model.ChatModel
@@ -57,20 +55,18 @@ fun ChatArchiveLayout(
) { ) {
Column( Column(
Modifier.fillMaxWidth(), Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.Start,
) { ) {
AppBarTitle(title) AppBarTitle(title)
SectionView(stringResource(R.string.chat_archive_section)) { SectionView(stringResource(R.string.chat_archive_section)) {
SettingsActionItem( SettingsActionItem(
Icons.Outlined.IosShare, painterResource(R.drawable.ic_ios_share),
stringResource(R.string.save_archive), stringResource(R.string.save_archive),
saveArchive, saveArchive,
textColor = MaterialTheme.colors.primary, textColor = MaterialTheme.colors.primary,
iconColor = MaterialTheme.colors.primary, iconColor = MaterialTheme.colors.primary,
) )
SectionDivider()
SettingsActionItem( SettingsActionItem(
Icons.Outlined.Delete, painterResource(R.drawable.ic_delete),
stringResource(R.string.delete_archive), stringResource(R.string.delete_archive),
deleteArchiveAlert, deleteArchiveAlert,
textColor = Color.Red, textColor = Color.Red,
@@ -81,6 +77,7 @@ fun ChatArchiveLayout(
SectionTextFooter( SectionTextFooter(
String.format(generalGetString(R.string.archive_created_on_ts), archiveTs) String.format(generalGetString(R.string.archive_created_on_ts), archiveTs)
) )
SectionBottomSpacer()
} }
} }
@@ -119,7 +116,8 @@ private fun deleteArchiveAlert(m: ChatModel, archivePath: String) {
} else { } else {
Log.e(TAG, "deleteArchiveAlert delete() error") Log.e(TAG, "deleteArchiveAlert delete() error")
} }
} },
destructive = true,
) )
} }
@@ -1,5 +1,6 @@
package chat.simplex.app.views.database package chat.simplex.app.views.database
import SectionBottomSpacer
import SectionItemView import SectionItemView
import SectionItemViewSpaceBetween import SectionItemViewSpaceBetween
import SectionTextFooter import SectionTextFooter
@@ -11,27 +12,25 @@ import androidx.compose.foundation.shape.ZeroCornerSize
import androidx.compose.foundation.text.* import androidx.compose.foundation.text.*
import androidx.compose.material.* import androidx.compose.material.*
import androidx.compose.material.TextFieldDefaults.indicatorLine import androidx.compose.material.TextFieldDefaults.indicatorLine
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material.icons.outlined.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.* import androidx.compose.ui.*
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.* import androidx.compose.ui.text.*
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.* import androidx.compose.ui.text.input.*
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.*
import androidx.compose.ui.unit.sp
import chat.simplex.app.R import chat.simplex.app.R
import chat.simplex.app.SimplexApp import chat.simplex.app.SimplexApp
import chat.simplex.app.model.* import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.* import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.helpers.* import chat.simplex.app.views.helpers.*
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.datetime.Clock import kotlinx.datetime.Clock
import kotlin.math.log2 import kotlin.math.log2
@@ -41,9 +40,9 @@ fun DatabaseEncryptionView(m: ChatModel) {
val prefs = m.controller.appPrefs val prefs = m.controller.appPrefs
val useKeychain = remember { mutableStateOf(prefs.storeDBPassphrase.get()) } val useKeychain = remember { mutableStateOf(prefs.storeDBPassphrase.get()) }
val initialRandomDBPassphrase = remember { mutableStateOf(prefs.initialRandomDBPassphrase.get()) } val initialRandomDBPassphrase = remember { mutableStateOf(prefs.initialRandomDBPassphrase.get()) }
val storedKey = remember { val key = DatabaseUtils.getDatabaseKey(); mutableStateOf(key != null && key != "") } val storedKey = remember { val key = DatabaseUtils.ksDatabasePassword.get(); mutableStateOf(key != null && key != "") }
// Do not do rememberSaveable on current key to prevent saving it on disk in clear text // Do not do rememberSaveable on current key to prevent saving it on disk in clear text
val currentKey = remember { mutableStateOf(if (initialRandomDBPassphrase.value) DatabaseUtils.getDatabaseKey() ?: "" else "") } val currentKey = remember { mutableStateOf(if (initialRandomDBPassphrase.value) DatabaseUtils.ksDatabasePassword.get() ?: "" else "") }
val newKey = rememberSaveable { mutableStateOf("") } val newKey = rememberSaveable { mutableStateOf("") }
val confirmNewKey = rememberSaveable { mutableStateOf("") } val confirmNewKey = rememberSaveable { mutableStateOf("") }
@@ -88,7 +87,7 @@ fun DatabaseEncryptionView(m: ChatModel) {
prefs.initialRandomDBPassphrase.set(false) prefs.initialRandomDBPassphrase.set(false)
initialRandomDBPassphrase.value = false initialRandomDBPassphrase.value = false
if (useKeychain.value) { if (useKeychain.value) {
DatabaseUtils.setDatabaseKey(newKey.value) DatabaseUtils.ksDatabasePassword.set(newKey.value)
} }
resetFormAfterEncryption(m, initialRandomDBPassphrase, currentKey, newKey, confirmNewKey, storedKey, useKeychain.value) resetFormAfterEncryption(m, initialRandomDBPassphrase, currentKey, newKey, confirmNewKey, storedKey, useKeychain.value)
operationEnded(m, progressIndicator) { operationEnded(m, progressIndicator) {
@@ -113,7 +112,7 @@ fun DatabaseEncryptionView(m: ChatModel) {
Modifier Modifier
.padding(horizontal = 2.dp) .padding(horizontal = 2.dp)
.size(30.dp), .size(30.dp),
color = HighOrLowlight, color = MaterialTheme.colors.secondary,
strokeWidth = 2.5.dp strokeWidth = 2.5.dp
) )
} }
@@ -136,7 +135,6 @@ fun DatabaseEncryptionLayout(
) { ) {
Column( Column(
Modifier.fillMaxWidth().verticalScroll(rememberScrollState()), Modifier.fillMaxWidth().verticalScroll(rememberScrollState()),
horizontalAlignment = Alignment.Start,
) { ) {
AppBarTitle(stringResource(R.string.database_passphrase)) AppBarTitle(stringResource(R.string.database_passphrase))
SectionView(null) { SectionView(null) {
@@ -149,7 +147,7 @@ fun DatabaseEncryptionLayout(
text = generalGetString(R.string.notifications_will_be_hidden) + "\n" + storeSecurelyDanger(), text = generalGetString(R.string.notifications_will_be_hidden) + "\n" + storeSecurelyDanger(),
confirmText = generalGetString(R.string.remove_passphrase), confirmText = generalGetString(R.string.remove_passphrase),
onConfirm = { onConfirm = {
DatabaseUtils.removeDatabaseKey() DatabaseUtils.ksDatabasePassword.remove()
setUseKeychain(false, useKeychain, prefs) setUseKeychain(false, useKeychain, prefs)
storedKey.value = false storedKey.value = false
}, },
@@ -161,7 +159,7 @@ fun DatabaseEncryptionLayout(
} }
if (!initialRandomDBPassphrase.value && chatDbEncrypted == true) { if (!initialRandomDBPassphrase.value && chatDbEncrypted == true) {
DatabaseKeyField( PassphraseField(
currentKey, currentKey,
generalGetString(R.string.current_passphrase), generalGetString(R.string.current_passphrase),
modifier = Modifier.padding(horizontal = DEFAULT_PADDING), modifier = Modifier.padding(horizontal = DEFAULT_PADDING),
@@ -170,7 +168,7 @@ fun DatabaseEncryptionLayout(
) )
} }
DatabaseKeyField( PassphraseField(
newKey, newKey,
generalGetString(R.string.new_passphrase), generalGetString(R.string.new_passphrase),
modifier = Modifier.padding(horizontal = DEFAULT_PADDING), modifier = Modifier.padding(horizontal = DEFAULT_PADDING),
@@ -201,7 +199,7 @@ fun DatabaseEncryptionLayout(
!validKey(newKey.value) || !validKey(newKey.value) ||
progressIndicator.value progressIndicator.value
DatabaseKeyField( PassphraseField(
confirmNewKey, confirmNewKey,
generalGetString(R.string.confirm_new_passphrase), generalGetString(R.string.confirm_new_passphrase),
modifier = Modifier.padding(horizontal = DEFAULT_PADDING), modifier = Modifier.padding(horizontal = DEFAULT_PADDING),
@@ -212,8 +210,8 @@ fun DatabaseEncryptionLayout(
}), }),
) )
SectionItemViewSpaceBetween(onClickUpdate, disabled = disabled) { SectionItemViewSpaceBetween(onClickUpdate, disabled = disabled, minHeight = TextFieldDefaults.MinHeight) {
Text(generalGetString(R.string.update_database_passphrase), color = if (disabled) HighOrLowlight else MaterialTheme.colors.primary) Text(generalGetString(R.string.update_database_passphrase), color = if (disabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary)
} }
} }
@@ -236,6 +234,7 @@ fun DatabaseEncryptionLayout(
SectionTextFooter(generalGetString(R.string.impossible_to_recover_passphrase)) SectionTextFooter(generalGetString(R.string.impossible_to_recover_passphrase))
} }
} }
SectionBottomSpacer()
} }
} }
@@ -245,7 +244,7 @@ fun encryptDatabaseSavedAlert(onConfirm: () -> Unit) {
text = generalGetString(R.string.database_will_be_encrypted_and_passphrase_stored) + "\n" + storeSecurelySaved(), text = generalGetString(R.string.database_will_be_encrypted_and_passphrase_stored) + "\n" + storeSecurelySaved(),
confirmText = generalGetString(R.string.encrypt_database), confirmText = generalGetString(R.string.encrypt_database),
onConfirm = onConfirm, onConfirm = onConfirm,
destructive = false, destructive = true,
) )
} }
@@ -285,14 +284,15 @@ fun SavePassphraseSetting(
initialRandomDBPassphrase: Boolean, initialRandomDBPassphrase: Boolean,
storedKey: Boolean, storedKey: Boolean,
progressIndicator: Boolean, progressIndicator: Boolean,
minHeight: Dp = TextFieldDefaults.MinHeight,
onCheckedChange: (Boolean) -> Unit, onCheckedChange: (Boolean) -> Unit,
) { ) {
SectionItemView { SectionItemView(minHeight = minHeight) {
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
Icon( Icon(
if (storedKey) Icons.Filled.VpnKey else Icons.Filled.VpnKeyOff, if (storedKey) painterResource(R.drawable.ic_vpn_key_filled) else painterResource(R.drawable.ic_vpn_key_off_filled),
stringResource(R.string.save_passphrase_in_keychain), stringResource(R.string.save_passphrase_in_keychain),
tint = if (storedKey) SimplexGreen else HighOrLowlight tint = if (storedKey) SimplexGreen else MaterialTheme.colors.secondary
) )
Spacer(Modifier.padding(horizontal = 4.dp)) Spacer(Modifier.padding(horizontal = 4.dp))
Text( Text(
@@ -301,13 +301,9 @@ fun SavePassphraseSetting(
color = Color.Unspecified color = Color.Unspecified
) )
Spacer(Modifier.fillMaxWidth().weight(1f)) Spacer(Modifier.fillMaxWidth().weight(1f))
Switch( DefaultSwitch(
checked = useKeychain, checked = useKeychain,
onCheckedChange = onCheckedChange, onCheckedChange = onCheckedChange,
colors = SwitchDefaults.colors(
checkedThumbColor = MaterialTheme.colors.primary,
uncheckedThumbColor = HighOrLowlight
),
enabled = !initialRandomDBPassphrase && !progressIndicator enabled = !initialRandomDBPassphrase && !progressIndicator
) )
} }
@@ -349,21 +345,22 @@ private fun operationEnded(m: ChatModel, progressIndicator: MutableState<Boolean
@OptIn(ExperimentalComposeUiApi::class) @OptIn(ExperimentalComposeUiApi::class)
@Composable @Composable
fun DatabaseKeyField( fun PassphraseField(
key: MutableState<String>, key: MutableState<String>,
placeholder: String, placeholder: String,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
showStrength: Boolean = false, showStrength: Boolean = false,
isValid: (String) -> Boolean, isValid: (String) -> Boolean,
keyboardActions: KeyboardActions = KeyboardActions(), keyboardActions: KeyboardActions = KeyboardActions(),
dependsOn: State<Any?>? = null,
) { ) {
var valid by remember { mutableStateOf(validKey(key.value)) } var valid by remember { mutableStateOf(validKey(key.value)) }
var showKey by remember { mutableStateOf(false) } var showKey by remember { mutableStateOf(false) }
val icon = if (valid) { val icon = if (valid) {
if (showKey) Icons.Filled.VisibilityOff else Icons.Filled.Visibility if (showKey) painterResource(R.drawable.ic_visibility_off_filled) else painterResource(R.drawable.ic_visibility_filled)
} else Icons.Outlined.Error } else painterResource(R.drawable.ic_error)
val iconColor = if (valid) { val iconColor = if (valid) {
if (showStrength && key.value.isNotEmpty()) PassphraseStrength.check(key.value).color else HighOrLowlight if (showStrength && key.value.isNotEmpty()) PassphraseStrength.check(key.value).color else MaterialTheme.colors.secondary
} else Color.Red } else Color.Red
val keyboard = LocalSoftwareKeyboardController.current val keyboard = LocalSoftwareKeyboardController.current
val keyboardOptions = KeyboardOptions( val keyboardOptions = KeyboardOptions(
@@ -420,7 +417,7 @@ fun DatabaseKeyField(
TextFieldDefaults.TextFieldDecorationBox( TextFieldDefaults.TextFieldDecorationBox(
value = state.value.text, value = state.value.text,
innerTextField = innerTextField, innerTextField = innerTextField,
placeholder = { Text(placeholder, color = HighOrLowlight) }, placeholder = { Text(placeholder, color = MaterialTheme.colors.secondary) },
singleLine = true, singleLine = true,
enabled = enabled, enabled = enabled,
isError = !valid, isError = !valid,
@@ -436,6 +433,13 @@ fun DatabaseKeyField(
) )
} }
) )
LaunchedEffect(Unit) {
snapshotFlow { dependsOn?.value }
.distinctUntilChanged()
.collect {
valid = isValid(state.value.text)
}
}
} }
// based on https://generatepasswords.org/how-to-calculate-entropy/ // based on https://generatepasswords.org/how-to-calculate-entropy/
@@ -461,7 +465,7 @@ private fun passphraseEntropy(s: String): Double {
return s.length * log2(poolSize.toDouble()) return s.length * log2(poolSize.toDouble())
} }
private enum class PassphraseStrength(val color: Color) { enum class PassphraseStrength(val color: Color) {
VERY_WEAK(Color.Red), WEAK(WarningOrange), REASONABLE(WarningYellow), STRONG(SimplexGreen); VERY_WEAK(Color.Red), WEAK(WarningOrange), REASONABLE(WarningYellow), STRONG(SimplexGreen);
companion object { companion object {
@@ -504,4 +508,4 @@ fun PreviewDatabaseEncryptionLayout() {
onConfirmEncrypt = {}, onConfirmEncrypt = {},
) )
} }
} }
@@ -1,9 +1,11 @@
package chat.simplex.app.views.database package chat.simplex.app.views.database
import SectionBottomSpacer
import SectionSpacer import SectionSpacer
import SectionView import SectionView
import android.content.Context import android.content.Context
import android.util.Log import android.util.Log
import androidx.annotation.StringRes
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardActions
@@ -13,6 +15,7 @@ import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import chat.simplex.app.* import chat.simplex.app.*
@@ -20,6 +23,7 @@ import chat.simplex.app.R
import chat.simplex.app.model.AppPreferences import chat.simplex.app.model.AppPreferences
import chat.simplex.app.ui.theme.* import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.helpers.* import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.usersettings.AppVersionText
import chat.simplex.app.views.usersettings.NotificationsMode import chat.simplex.app.views.usersettings.NotificationsMode
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.datetime.Clock import kotlinx.datetime.Clock
@@ -35,94 +39,137 @@ fun DatabaseErrorView(
) { ) {
val progressIndicator = remember { mutableStateOf(false) } val progressIndicator = remember { mutableStateOf(false) }
val dbKey = remember { mutableStateOf("") } val dbKey = remember { mutableStateOf("") }
var storedDBKey by remember { mutableStateOf(DatabaseUtils.getDatabaseKey()) } var storedDBKey by remember { mutableStateOf(DatabaseUtils.ksDatabasePassword.get()) }
var useKeychain by remember { mutableStateOf(appPreferences.storeDBPassphrase.get()) } var useKeychain by remember { mutableStateOf(appPreferences.storeDBPassphrase.get()) }
val context = LocalContext.current val context = LocalContext.current
val restoreDbFromBackup = remember { mutableStateOf(shouldShowRestoreDbButton(appPreferences, context)) } val restoreDbFromBackup = remember { mutableStateOf(shouldShowRestoreDbButton(appPreferences, context)) }
val saveAndRunChatOnClick: () -> Unit = {
DatabaseUtils.setDatabaseKey(dbKey.value) fun callRunChat(confirmMigrations: MigrationConfirmation? = null) {
val useKey = if (useKeychain) null else dbKey.value
runChat(useKey, confirmMigrations, chatDbStatus, progressIndicator, appPreferences)
}
fun saveAndRunChatOnClick() {
DatabaseUtils.ksDatabasePassword.set(dbKey.value)
storedDBKey = dbKey.value storedDBKey = dbKey.value
appPreferences.storeDBPassphrase.set(true) appPreferences.storeDBPassphrase.set(true)
useKeychain = true useKeychain = true
appPreferences.initialRandomDBPassphrase.set(false) appPreferences.initialRandomDBPassphrase.set(false)
runChat(dbKey.value, chatDbStatus, progressIndicator, appPreferences) callRunChat()
} }
val title = when (chatDbStatus.value) {
is DBMigrationResult.OK -> "" @Composable
is DBMigrationResult.ErrorNotADatabase -> if (useKeychain && !storedDBKey.isNullOrEmpty()) fun DatabaseErrorDetails(@StringRes title: Int, content: @Composable ColumnScope.() -> Unit) {
generalGetString(R.string.wrong_passphrase) Text(
else generalGetString(title),
generalGetString(R.string.encrypted_database) Modifier.padding(start = DEFAULT_PADDING, top = DEFAULT_PADDING, bottom = DEFAULT_PADDING),
is DBMigrationResult.Error -> generalGetString(R.string.database_error) style = MaterialTheme.typography.h1
is DBMigrationResult.ErrorKeychain -> generalGetString(R.string.keychain_error) )
is DBMigrationResult.Unknown -> generalGetString(R.string.database_error) SectionView(null, padding = PaddingValues(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF), content)
null -> "" // should never be here }
@Composable
fun FileNameText(dbFile: String) {
Text(String.format(generalGetString(R.string.file_with_path), dbFile.split("/").lastOrNull() ?: dbFile))
}
@Composable
fun MigrationsText(ms: List<String>) {
Text(String.format(generalGetString(R.string.database_migrations), ms.joinToString(", ")))
} }
Column( Column(
Modifier.fillMaxSize().verticalScroll(rememberScrollState()), Modifier.fillMaxSize().verticalScroll(rememberScrollState()),
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.Center, verticalArrangement = Arrangement.Center,
) { ) {
Text( val buttonEnabled = validKey(dbKey.value) && !progressIndicator.value
title, when (val status = chatDbStatus.value) {
Modifier.padding(start = 16.dp, top = 16.dp, bottom = 24.dp), is DBMigrationResult.ErrorNotADatabase ->
style = MaterialTheme.typography.h1 if (useKeychain && !storedDBKey.isNullOrEmpty()) {
) DatabaseErrorDetails(R.string.wrong_passphrase) {
SectionView(null, padding = PaddingValues(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF)) {
val buttonEnabled = validKey(dbKey.value) && !progressIndicator.value
when (val status = chatDbStatus.value) {
is DBMigrationResult.ErrorNotADatabase -> {
if (useKeychain && !storedDBKey.isNullOrEmpty()) {
Text(generalGetString(R.string.passphrase_is_different)) Text(generalGetString(R.string.passphrase_is_different))
DatabaseKeyField(dbKey, buttonEnabled) { DatabaseKeyField(dbKey, buttonEnabled) {
saveAndRunChatOnClick() saveAndRunChatOnClick()
} }
SaveAndOpenButton(buttonEnabled, saveAndRunChatOnClick) SaveAndOpenButton(buttonEnabled, ::saveAndRunChatOnClick)
SectionSpacer() SectionSpacer()
Text(String.format(generalGetString(R.string.file_with_path), status.dbFile)) FileNameText(status.dbFile)
} else { }
} else {
DatabaseErrorDetails(R.string.encrypted_database) {
Text(generalGetString(R.string.database_passphrase_is_required)) Text(generalGetString(R.string.database_passphrase_is_required))
DatabaseKeyField(dbKey, buttonEnabled) {
if (useKeychain) saveAndRunChatOnClick() else runChat(dbKey.value, chatDbStatus, progressIndicator, appPreferences)
}
if (useKeychain) { if (useKeychain) {
SaveAndOpenButton(buttonEnabled, saveAndRunChatOnClick) DatabaseKeyField(dbKey, buttonEnabled, ::saveAndRunChatOnClick)
SaveAndOpenButton(buttonEnabled, ::saveAndRunChatOnClick)
} else { } else {
OpenChatButton(buttonEnabled) { runChat(dbKey.value, chatDbStatus, progressIndicator, appPreferences) } DatabaseKeyField(dbKey, buttonEnabled) { callRunChat() }
OpenChatButton(buttonEnabled) { callRunChat() }
} }
} }
} }
is DBMigrationResult.Error -> { is DBMigrationResult.ErrorMigration -> when (val err = status.migrationError) {
Text(String.format(generalGetString(R.string.file_with_path), status.dbFile)) is MigrationError.Upgrade ->
Text(String.format(generalGetString(R.string.error_with_info), status.migrationError)) DatabaseErrorDetails(R.string.database_upgrade) {
TextButton({ callRunChat(confirmMigrations = MigrationConfirmation.YesUp) }, Modifier.align(Alignment.CenterHorizontally), enabled = !progressIndicator.value) {
Text(generalGetString(R.string.upgrade_and_open_chat))
}
Spacer(Modifier.height(20.dp))
FileNameText(status.dbFile)
MigrationsText(err.upMigrations.map { it.upName })
AppVersionText()
}
is MigrationError.Downgrade ->
DatabaseErrorDetails(R.string.database_downgrade) {
TextButton({ callRunChat(confirmMigrations = MigrationConfirmation.YesUpDown) }, Modifier.align(Alignment.CenterHorizontally), enabled = !progressIndicator.value) {
Text(generalGetString(R.string.downgrade_and_open_chat))
}
Spacer(Modifier.height(20.dp))
Text(generalGetString(R.string.database_downgrade_warning), fontWeight = FontWeight.Bold)
FileNameText(status.dbFile)
MigrationsText(err.downMigrations)
AppVersionText()
}
is MigrationError.Error ->
DatabaseErrorDetails(R.string.incompatible_database_version) {
FileNameText(status.dbFile)
Text(String.format(generalGetString(R.string.error_with_info), mtrErrorDescription(err.mtrError)))
}
}
is DBMigrationResult.ErrorSQL ->
DatabaseErrorDetails(R.string.database_error) {
FileNameText(status.dbFile)
Text(String.format(generalGetString(R.string.error_with_info), status.migrationSQLError))
} }
is DBMigrationResult.ErrorKeychain -> { is DBMigrationResult.ErrorKeychain ->
DatabaseErrorDetails(R.string.keychain_error) {
Text(generalGetString(R.string.cannot_access_keychain)) Text(generalGetString(R.string.cannot_access_keychain))
} }
is DBMigrationResult.Unknown -> { is DBMigrationResult.InvalidConfirmation ->
DatabaseErrorDetails(R.string.invalid_migration_confirmation) {
// this can only happen if incorrect parameter is passed
}
is DBMigrationResult.Unknown ->
DatabaseErrorDetails(R.string.database_error) {
Text(String.format(generalGetString(R.string.unknown_database_error_with_info), status.json)) Text(String.format(generalGetString(R.string.unknown_database_error_with_info), status.json))
} }
is DBMigrationResult.OK -> { is DBMigrationResult.OK -> {}
} null -> {}
null -> { }
} if (restoreDbFromBackup.value) {
} SectionSpacer()
if (restoreDbFromBackup.value) { Text(generalGetString(R.string.database_backup_can_be_restored))
SectionSpacer() Spacer(Modifier.size(DEFAULT_PADDING))
Text(generalGetString(R.string.database_backup_can_be_restored)) RestoreDbButton {
Spacer(Modifier.size(16.dp)) AlertManager.shared.showAlertDialog(
RestoreDbButton { title = generalGetString(R.string.restore_database_alert_title),
AlertManager.shared.showAlertDialog( text = generalGetString(R.string.restore_database_alert_desc),
title = generalGetString(R.string.restore_database_alert_title), confirmText = generalGetString(R.string.restore_database_alert_confirm),
text = generalGetString(R.string.restore_database_alert_desc), onConfirm = { restoreDb(restoreDbFromBackup, appPreferences, context) },
confirmText = generalGetString(R.string.restore_database_alert_confirm), destructive = true,
onConfirm = { restoreDb(restoreDbFromBackup, appPreferences, context) }, )
destructive = true,
)
}
} }
} }
SectionBottomSpacer()
} }
if (progressIndicator.value) { if (progressIndicator.value) {
Box( Box(
@@ -133,7 +180,7 @@ fun DatabaseErrorView(
Modifier Modifier
.padding(horizontal = 2.dp) .padding(horizontal = 2.dp)
.size(30.dp), .size(30.dp),
color = HighOrLowlight, color = MaterialTheme.colors.secondary,
strokeWidth = 2.5.dp strokeWidth = 2.5.dp
) )
} }
@@ -141,7 +188,8 @@ fun DatabaseErrorView(
} }
private fun runChat( private fun runChat(
dbKey: String, dbKey: String? = null,
confirmMigrations: MigrationConfirmation? = null,
chatDbStatus: State<DBMigrationResult?>, chatDbStatus: State<DBMigrationResult?>,
progressIndicator: MutableState<Boolean>, progressIndicator: MutableState<Boolean>,
prefs: AppPreferences prefs: AppPreferences
@@ -150,7 +198,7 @@ private fun runChat(
if (progressIndicator.value) return@launch if (progressIndicator.value) return@launch
progressIndicator.value = true progressIndicator.value = true
try { try {
SimplexApp.context.initChatController(dbKey) SimplexApp.context.initChatController(dbKey, confirmMigrations)
} catch (e: Exception) { } catch (e: Exception) {
Log.d(TAG, "initializeChat ${e.stackTraceToString()}") Log.d(TAG, "initializeChat ${e.stackTraceToString()}")
} }
@@ -163,18 +211,17 @@ private fun runChat(
NotificationsMode.PERIODIC.name -> SimplexApp.context.schedulePeriodicWakeUp() NotificationsMode.PERIODIC.name -> SimplexApp.context.schedulePeriodicWakeUp()
} }
} }
is DBMigrationResult.ErrorNotADatabase -> { is DBMigrationResult.ErrorNotADatabase ->
AlertManager.shared.showAlertMsg(generalGetString(R.string.wrong_passphrase_title), generalGetString(R.string.enter_correct_passphrase)) AlertManager.shared.showAlertMsg(generalGetString(R.string.wrong_passphrase_title), generalGetString(R.string.enter_correct_passphrase))
} is DBMigrationResult.ErrorSQL ->
is DBMigrationResult.Error -> { AlertManager.shared.showAlertMsg(generalGetString(R.string.database_error), status.migrationSQLError)
AlertManager.shared.showAlertMsg(generalGetString(R.string.database_error), status.migrationError) is DBMigrationResult.ErrorKeychain ->
}
is DBMigrationResult.ErrorKeychain -> {
AlertManager.shared.showAlertMsg(generalGetString(R.string.keychain_error)) AlertManager.shared.showAlertMsg(generalGetString(R.string.keychain_error))
} is DBMigrationResult.Unknown ->
is DBMigrationResult.Unknown -> {
AlertManager.shared.showAlertMsg(generalGetString(R.string.unknown_error), status.json) AlertManager.shared.showAlertMsg(generalGetString(R.string.unknown_error), status.json)
} is DBMigrationResult.InvalidConfirmation ->
AlertManager.shared.showAlertMsg(generalGetString(R.string.invalid_migration_confirmation))
is DBMigrationResult.ErrorMigration -> {}
null -> {} null -> {}
} }
} }
@@ -204,9 +251,17 @@ private fun restoreDb(restoreDbFromBackup: MutableState<Boolean>, prefs: AppPref
} }
} }
private fun mtrErrorDescription(err: MTRError): String =
when (err) {
is MTRError.NoDown ->
String.format(generalGetString(R.string.mtr_error_no_down_migration), err.dbMigrations.joinToString(", "))
is MTRError.Different ->
String.format(generalGetString(R.string.mtr_error_different), err.appMigration, err.dbMigration)
}
@Composable @Composable
private fun DatabaseKeyField(text: MutableState<String>, enabled: Boolean, onClick: (() -> Unit)? = null) { private fun DatabaseKeyField(text: MutableState<String>, enabled: Boolean, onClick: (() -> Unit)? = null) {
DatabaseKeyField( PassphraseField(
text, text,
generalGetString(R.string.enter_passphrase), generalGetString(R.string.enter_passphrase),
isValid = ::validKey, isValid = ::validKey,
@@ -1,14 +1,13 @@
package chat.simplex.app.views.database package chat.simplex.app.views.database
import SectionDivider import SectionBottomSpacer
import SectionDividerSpaced
import SectionTextFooter import SectionTextFooter
import SectionItemView import SectionItemView
import SectionSpacer
import SectionView import SectionView
import android.content.Context import android.content.Context
import android.content.res.Configuration import android.content.res.Configuration
import android.net.Uri import android.net.Uri
import android.os.FileUtils
import android.util.Log import android.util.Log
import android.widget.Toast import android.widget.Toast
import androidx.activity.compose.ManagedActivityResultLauncher import androidx.activity.compose.ManagedActivityResultLauncher
@@ -18,15 +17,15 @@ import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.* import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material.icons.outlined.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.*
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
@@ -38,6 +37,7 @@ import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.usersettings.* import chat.simplex.app.views.usersettings.*
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.datetime.* import kotlinx.datetime.*
import org.apache.commons.io.IOUtils
import java.io.* import java.io.*
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.*
@@ -50,7 +50,7 @@ fun DatabaseView(
) { ) {
val context = LocalContext.current val context = LocalContext.current
val progressIndicator = remember { mutableStateOf(false) } val progressIndicator = remember { mutableStateOf(false) }
val runChat = remember { mutableStateOf(m.chatRunning.value ?: true) } val runChat = remember { m.chatRunning }
val prefs = m.controller.appPrefs val prefs = m.controller.appPrefs
val useKeychain = remember { mutableStateOf(prefs.storeDBPassphrase.get()) } val useKeychain = remember { mutableStateOf(prefs.storeDBPassphrase.get()) }
val chatArchiveName = remember { mutableStateOf(prefs.chatArchiveName.get()) } val chatArchiveName = remember { mutableStateOf(prefs.chatArchiveName.get()) }
@@ -74,7 +74,7 @@ fun DatabaseView(
) { ) {
DatabaseLayout( DatabaseLayout(
progressIndicator.value, progressIndicator.value,
runChat.value, runChat.value != false,
m.chatDbChanged.value, m.chatDbChanged.value,
useKeychain.value, useKeychain.value,
m.chatDbEncrypted.value, m.chatDbEncrypted.value,
@@ -87,6 +87,8 @@ fun DatabaseView(
m.controller.appPrefs.privacyFullBackup, m.controller.appPrefs.privacyFullBackup,
appFilesCountAndSize, appFilesCountAndSize,
chatItemTTL, chatItemTTL,
m.currentUser.value,
m.users,
startChat = { startChat(m, runChat, chatLastStart, m.chatDbChanged) }, startChat = { startChat(m, runChat, chatLastStart, m.chatDbChanged) },
stopChatAlert = { stopChatAlert(m, runChat, context) }, stopChatAlert = { stopChatAlert(m, runChat, context) },
exportArchive = { exportArchive(context, m, progressIndicator, chatArchiveName, chatArchiveTime, chatArchiveFile, saveArchiveLauncher) }, exportArchive = { exportArchive(context, m, progressIndicator, chatArchiveName, chatArchiveTime, chatArchiveFile, saveArchiveLauncher) },
@@ -112,7 +114,7 @@ fun DatabaseView(
Modifier Modifier
.padding(horizontal = 2.dp) .padding(horizontal = 2.dp)
.size(30.dp), .size(30.dp),
color = HighOrLowlight, color = MaterialTheme.colors.secondary,
strokeWidth = 2.5.dp strokeWidth = 2.5.dp
) )
} }
@@ -136,6 +138,8 @@ fun DatabaseLayout(
privacyFullBackup: SharedPreference<Boolean>, privacyFullBackup: SharedPreference<Boolean>,
appFilesCountAndSize: MutableState<Pair<Int, Long>>, appFilesCountAndSize: MutableState<Pair<Int, Long>>,
chatItemTTL: MutableState<ChatItemTTL>, chatItemTTL: MutableState<ChatItemTTL>,
currentUser: User?,
users: List<UserInfo>,
startChat: () -> Unit, startChat: () -> Unit,
stopChatAlert: () -> Unit, stopChatAlert: () -> Unit,
exportArchive: () -> Unit, exportArchive: () -> Unit,
@@ -149,28 +153,45 @@ fun DatabaseLayout(
Column( Column(
Modifier.fillMaxWidth().verticalScroll(rememberScrollState()), Modifier.fillMaxWidth().verticalScroll(rememberScrollState()),
horizontalAlignment = Alignment.Start,
) { ) {
AppBarTitle(stringResource(R.string.your_chat_database)) AppBarTitle(stringResource(R.string.your_chat_database))
SectionView(stringResource(R.string.messages_section_title).uppercase()) {
TtlOptions(chatItemTTL, enabled = rememberUpdatedState(!stopped && !progressIndicator), onChatItemTTLSelected)
}
SectionTextFooter(
remember(currentUser?.displayName) {
buildAnnotatedString {
append(generalGetString(R.string.messages_section_description) + " ")
withStyle(SpanStyle(fontWeight = FontWeight.Bold)) {
append(currentUser?.displayName ?: "")
}
append(".")
}
}
)
SectionDividerSpaced(maxTopPadding = true)
SectionView(stringResource(R.string.run_chat_section)) { SectionView(stringResource(R.string.run_chat_section)) {
RunChatSetting(runChat, stopped, chatDbDeleted, startChat, stopChatAlert) RunChatSetting(runChat, stopped, chatDbDeleted, startChat, stopChatAlert)
} }
SectionSpacer() SectionDividerSpaced()
SectionView(stringResource(R.string.chat_database_section)) { SectionView(stringResource(R.string.chat_database_section)) {
val unencrypted = chatDbEncrypted == false val unencrypted = chatDbEncrypted == false
SettingsActionItem( SettingsActionItem(
if (unencrypted) Icons.Outlined.LockOpen else if (useKeyChain) Icons.Filled.VpnKey else Icons.Outlined.Lock, if (unencrypted) painterResource(R.drawable.ic_lock_open) else if (useKeyChain) painterResource(R.drawable.ic_vpn_key_filled)
else painterResource(R
.drawable.ic_lock),
stringResource(R.string.database_passphrase), stringResource(R.string.database_passphrase),
click = showSettingsModal() { DatabaseEncryptionView(it) }, click = showSettingsModal() { DatabaseEncryptionView(it) },
iconColor = if (unencrypted) WarningOrange else HighOrLowlight, iconColor = if (unencrypted) WarningOrange else MaterialTheme.colors.secondary,
disabled = operationsDisabled disabled = operationsDisabled
) )
SectionDivider() AppDataBackupPreference(privacyFullBackup, initialRandomDBPassphrase)
SettingsPreferenceItem(Icons.Outlined.Backup, stringResource(R.string.full_backup), privacyFullBackup) SectionDividerSpaced(maxBottomPadding = false)
SectionDivider()
SettingsActionItem( SettingsActionItem(
Icons.Outlined.IosShare, painterResource(R.drawable.ic_ios_share),
stringResource(R.string.export_database), stringResource(R.string.export_database),
click = { click = {
if (initialRandomDBPassphrase.get()) { if (initialRandomDBPassphrase.get()) {
@@ -183,31 +204,28 @@ fun DatabaseLayout(
iconColor = MaterialTheme.colors.primary, iconColor = MaterialTheme.colors.primary,
disabled = operationsDisabled disabled = operationsDisabled
) )
SectionDivider()
SettingsActionItem( SettingsActionItem(
Icons.Outlined.FileDownload, painterResource(R.drawable.ic_download),
stringResource(R.string.import_database), stringResource(R.string.import_database),
{ importArchiveLauncher.launch("application/zip") }, { importArchiveLauncher.launch("application/zip") },
textColor = Color.Red, textColor = Color.Red,
iconColor = Color.Red, iconColor = Color.Red,
disabled = operationsDisabled disabled = operationsDisabled
) )
SectionDivider()
val chatArchiveNameVal = chatArchiveName.value val chatArchiveNameVal = chatArchiveName.value
val chatArchiveTimeVal = chatArchiveTime.value val chatArchiveTimeVal = chatArchiveTime.value
val chatLastStartVal = chatLastStart.value val chatLastStartVal = chatLastStart.value
if (chatArchiveNameVal != null && chatArchiveTimeVal != null && chatLastStartVal != null) { if (chatArchiveNameVal != null && chatArchiveTimeVal != null && chatLastStartVal != null) {
val title = chatArchiveTitle(chatArchiveTimeVal, chatLastStartVal) val title = chatArchiveTitle(chatArchiveTimeVal, chatLastStartVal)
SettingsActionItem( SettingsActionItem(
Icons.Outlined.Inventory2, painterResource(R.drawable.ic_inventory_2),
title, title,
click = showSettingsModal { ChatArchiveView(it, title, chatArchiveNameVal, chatArchiveTimeVal) }, click = showSettingsModal { ChatArchiveView(it, title, chatArchiveNameVal, chatArchiveTimeVal) },
disabled = operationsDisabled disabled = operationsDisabled
) )
SectionDivider()
} }
SettingsActionItem( SettingsActionItem(
Icons.Outlined.DeleteForever, painterResource(R.drawable.ic_delete_forever),
stringResource(R.string.delete_database), stringResource(R.string.delete_database),
deleteChatAlert, deleteChatAlert,
textColor = Color.Red, textColor = Color.Red,
@@ -222,19 +240,17 @@ fun DatabaseLayout(
stringResource(R.string.stop_chat_to_enable_database_actions) stringResource(R.string.stop_chat_to_enable_database_actions)
} }
) )
SectionSpacer() SectionDividerSpaced(maxTopPadding = true)
SectionView(stringResource(R.string.data_section)) { SectionView(stringResource(R.string.files_and_media_section).uppercase()) {
SectionItemView { TtlOptions(chatItemTTL, enabled = rememberUpdatedState(!progressIndicator && !chatDbChanged), onChatItemTTLSelected) }
SectionDivider()
val deleteFilesDisabled = operationsDisabled || appFilesCountAndSize.value.first == 0 val deleteFilesDisabled = operationsDisabled || appFilesCountAndSize.value.first == 0
SectionItemView( SectionItemView(
deleteAppFilesAndMedia, deleteAppFilesAndMedia,
disabled = deleteFilesDisabled disabled = deleteFilesDisabled
) { ) {
Text( Text(
stringResource(R.string.delete_files_and_media), stringResource(if (users.size > 1) R.string.delete_files_and_media_for_all_users else R.string.delete_files_and_media_all),
color = if (deleteFilesDisabled) HighOrLowlight else Color.Red color = if (deleteFilesDisabled) MaterialTheme.colors.secondary else Color.Red
) )
} }
} }
@@ -246,6 +262,24 @@ fun DatabaseLayout(
String.format(stringResource(R.string.total_files_count_and_size), count, formatBytes(size)) String.format(stringResource(R.string.total_files_count_and_size), count, formatBytes(size))
} }
) )
SectionBottomSpacer()
}
}
@Composable
private fun AppDataBackupPreference(privacyFullBackup: SharedPreference<Boolean>, initialRandomDBPassphrase: SharedPreference<Boolean>) {
SettingsPreferenceItem(
painterResource(R.drawable.ic_backup),
iconColor = MaterialTheme.colors.secondary,
pref = privacyFullBackup,
text = stringResource(R.string.full_backup)
) {
if (initialRandomDBPassphrase.get()) {
exportProhibitedAlert()
privacyFullBackup.set(false)
} else {
privacyFullBackup.set(it)
}
} }
} }
@@ -260,7 +294,8 @@ private fun setChatItemTTLAlert(
text = generalGetString(R.string.enable_automatic_deletion_message), text = generalGetString(R.string.enable_automatic_deletion_message),
confirmText = generalGetString(R.string.delete_messages), confirmText = generalGetString(R.string.delete_messages),
onConfirm = { setCiTTL(m, selectedChatItemTTL, progressIndicator, appFilesCountAndSize, context) }, onConfirm = { setCiTTL(m, selectedChatItemTTL, progressIndicator, appFilesCountAndSize, context) },
onDismiss = { selectedChatItemTTL.value = m.chatItemTTL.value } onDismiss = { selectedChatItemTTL.value = m.chatItemTTL.value },
destructive = true,
) )
} }
@@ -299,36 +334,23 @@ fun RunChatSetting(
startChat: () -> Unit, startChat: () -> Unit,
stopChatAlert: () -> Unit stopChatAlert: () -> Unit
) { ) {
SectionItemView() { val chatRunningText = if (stopped) stringResource(R.string.chat_is_stopped) else stringResource(R.string.chat_is_running)
Row(verticalAlignment = Alignment.CenterVertically) { SettingsActionItemWithContent(
val chatRunningText = if (stopped) stringResource(R.string.chat_is_stopped) else stringResource(R.string.chat_is_running) icon = if (stopped) painterResource(R.drawable.ic_report_filled) else painterResource(R.drawable.ic_play_arrow_filled),
Icon( text = chatRunningText,
if (stopped) Icons.Filled.Report else Icons.Filled.PlayArrow, iconColor = if (stopped) Color.Red else MaterialTheme.colors.primary,
chatRunningText, ) {
tint = if (stopped) Color.Red else MaterialTheme.colors.primary DefaultSwitch(
) enabled = !chatDbDeleted,
Spacer(Modifier.padding(horizontal = 4.dp)) checked = runChat,
Text( onCheckedChange = { runChatSwitch ->
chatRunningText, if (runChatSwitch) {
Modifier.padding(end = 24.dp) startChat()
) } else {
Spacer(Modifier.fillMaxWidth().weight(1f)) stopChatAlert()
Switch( }
enabled = !chatDbDeleted, },
checked = runChat, )
onCheckedChange = { runChatSwitch ->
if (runChatSwitch) {
startChat()
} else {
stopChatAlert()
}
},
colors = SwitchDefaults.colors(
checkedThumbColor = MaterialTheme.colors.primary,
uncheckedThumbColor = HighOrLowlight
),
)
}
} }
} }
@@ -337,7 +359,7 @@ fun chatArchiveTitle(chatArchiveTime: Instant, chatLastStart: Instant): String {
return stringResource(if (chatArchiveTime < chatLastStart) R.string.old_database_archive else R.string.new_database_archive) return stringResource(if (chatArchiveTime < chatLastStart) R.string.old_database_archive else R.string.new_database_archive)
} }
private fun startChat(m: ChatModel, runChat: MutableState<Boolean>, chatLastStart: MutableState<Instant?>, chatDbChanged: MutableState<Boolean>) { private fun startChat(m: ChatModel, runChat: MutableState<Boolean?>, chatLastStart: MutableState<Instant?>, chatDbChanged: MutableState<Boolean>) {
withApi { withApi {
try { try {
if (chatDbChanged.value) { if (chatDbChanged.value) {
@@ -366,7 +388,7 @@ private fun startChat(m: ChatModel, runChat: MutableState<Boolean>, chatLastStar
} }
} }
private fun stopChatAlert(m: ChatModel, runChat: MutableState<Boolean>, context: Context) { private fun stopChatAlert(m: ChatModel, runChat: MutableState<Boolean?>, context: Context) {
AlertManager.shared.showAlertDialog( AlertManager.shared.showAlertDialog(
title = generalGetString(R.string.stop_chat_question), title = generalGetString(R.string.stop_chat_question),
text = generalGetString(R.string.stop_chat_to_export_import_or_delete_chat_database), text = generalGetString(R.string.stop_chat_to_export_import_or_delete_chat_database),
@@ -383,7 +405,7 @@ private fun exportProhibitedAlert() {
) )
} }
private fun authStopChat(m: ChatModel, runChat: MutableState<Boolean>, context: Context) { private fun authStopChat(m: ChatModel, runChat: MutableState<Boolean?>, context: Context) {
if (m.controller.appPrefs.performLA.get()) { if (m.controller.appPrefs.performLA.get()) {
authenticate( authenticate(
generalGetString(R.string.auth_stop_chat), generalGetString(R.string.auth_stop_chat),
@@ -391,12 +413,13 @@ private fun authStopChat(m: ChatModel, runChat: MutableState<Boolean>, context:
context as FragmentActivity, context as FragmentActivity,
completed = { laResult -> completed = { laResult ->
when (laResult) { when (laResult) {
LAResult.Success, LAResult.Unavailable -> { LAResult.Success, is LAResult.Unavailable -> {
stopChat(m, runChat, context) stopChat(m, runChat, context)
} }
is LAResult.Error -> { is LAResult.Error -> {
runChat.value = true
} }
LAResult.Failed -> { is LAResult.Failed -> {
runChat.value = true runChat.value = true
} }
} }
@@ -407,7 +430,7 @@ private fun authStopChat(m: ChatModel, runChat: MutableState<Boolean>, context:
} }
} }
private fun stopChat(m: ChatModel, runChat: MutableState<Boolean>, context: Context) { private fun stopChat(m: ChatModel, runChat: MutableState<Boolean?>, context: Context) {
withApi { withApi {
try { try {
m.controller.apiStopChat() m.controller.apiStopChat()
@@ -521,7 +544,8 @@ private fun importArchiveAlert(
title = generalGetString(R.string.import_database_question), title = generalGetString(R.string.import_database_question),
text = generalGetString(R.string.your_current_chat_database_will_be_deleted_and_replaced_with_the_imported_one), text = generalGetString(R.string.your_current_chat_database_will_be_deleted_and_replaced_with_the_imported_one),
confirmText = generalGetString(R.string.import_database_confirmation), confirmText = generalGetString(R.string.import_database_confirmation),
onConfirm = { importArchive(m, context, importedArchiveUri, appFilesCountAndSize, progressIndicator) } onConfirm = { importArchive(m, context, importedArchiveUri, appFilesCountAndSize, progressIndicator) },
destructive = true,
) )
} }
@@ -541,7 +565,7 @@ private fun importArchive(
try { try {
val config = ArchiveConfig(archivePath, parentTempDirectory = context.cacheDir.toString()) val config = ArchiveConfig(archivePath, parentTempDirectory = context.cacheDir.toString())
m.controller.apiImportArchive(config) m.controller.apiImportArchive(config)
DatabaseUtils.removeDatabaseKey() DatabaseUtils.ksDatabasePassword.remove()
appFilesCountAndSize.value = directoryFileCountAndSize(getAppFilesDirectory(context)) appFilesCountAndSize.value = directoryFileCountAndSize(getAppFilesDirectory(context))
operationEnded(m, progressIndicator) { operationEnded(m, progressIndicator) {
AlertManager.shared.showAlertMsg(generalGetString(R.string.chat_database_imported), generalGetString(R.string.restart_the_app_to_use_imported_chat_database)) AlertManager.shared.showAlertMsg(generalGetString(R.string.chat_database_imported), generalGetString(R.string.restart_the_app_to_use_imported_chat_database))
@@ -569,7 +593,7 @@ private fun saveArchiveFromUri(context: Context, importedArchiveUri: Uri): Strin
if (inputStream != null && archiveName != null) { if (inputStream != null && archiveName != null) {
val archivePath = "${context.cacheDir}/$archiveName" val archivePath = "${context.cacheDir}/$archiveName"
val destFile = File(archivePath) val destFile = File(archivePath)
FileUtils.copy(inputStream, FileOutputStream(destFile)) IOUtils.copy(inputStream, FileOutputStream(destFile))
archivePath archivePath
} else { } else {
Log.e(TAG, "saveArchiveFromUri null inputStream") Log.e(TAG, "saveArchiveFromUri null inputStream")
@@ -586,7 +610,8 @@ private fun deleteChatAlert(m: ChatModel, progressIndicator: MutableState<Boolea
title = generalGetString(R.string.delete_chat_profile_question), title = generalGetString(R.string.delete_chat_profile_question),
text = generalGetString(R.string.delete_chat_profile_action_cannot_be_undone_warning), text = generalGetString(R.string.delete_chat_profile_action_cannot_be_undone_warning),
confirmText = generalGetString(R.string.delete_verb), confirmText = generalGetString(R.string.delete_verb),
onConfirm = { deleteChat(m, progressIndicator) } onConfirm = { deleteChat(m, progressIndicator) },
destructive = true,
) )
} }
@@ -596,7 +621,7 @@ private fun deleteChat(m: ChatModel, progressIndicator: MutableState<Boolean>) {
try { try {
m.controller.apiDeleteStorage() m.controller.apiDeleteStorage()
m.chatDbDeleted.value = true m.chatDbDeleted.value = true
DatabaseUtils.removeDatabaseKey() DatabaseUtils.ksDatabasePassword.remove()
m.controller.appPrefs.storeDBPassphrase.set(true) m.controller.appPrefs.storeDBPassphrase.set(true)
operationEnded(m, progressIndicator) { operationEnded(m, progressIndicator) {
AlertManager.shared.showAlertMsg(generalGetString(R.string.chat_database_deleted), generalGetString(R.string.restart_the_app_to_create_a_new_chat_profile)) AlertManager.shared.showAlertMsg(generalGetString(R.string.chat_database_deleted), generalGetString(R.string.restart_the_app_to_create_a_new_chat_profile))
@@ -696,6 +721,8 @@ fun PreviewDatabaseLayout() {
privacyFullBackup = SharedPreference({ true }, {}), privacyFullBackup = SharedPreference({ true }, {}),
appFilesCountAndSize = remember { mutableStateOf(0 to 0L) }, appFilesCountAndSize = remember { mutableStateOf(0 to 0L) },
chatItemTTL = remember { mutableStateOf(ChatItemTTL.None) }, chatItemTTL = remember { mutableStateOf(ChatItemTTL.None) },
currentUser = User.sampleData,
users = listOf(UserInfo.sampleData),
startChat = {}, startChat = {},
stopChatAlert = {}, stopChatAlert = {},
exportArchive = {}, exportArchive = {},
@@ -3,16 +3,20 @@ package chat.simplex.app.views.helpers
import android.util.Log import android.util.Log
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CornerSize
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.* import androidx.compose.material.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.* import androidx.compose.ui.unit.*
import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.Dialog
import chat.simplex.app.R import chat.simplex.app.R
import chat.simplex.app.TAG import chat.simplex.app.TAG
import chat.simplex.app.ui.theme.DEFAULT_PADDING import chat.simplex.app.ui.theme.*
class AlertManager { class AlertManager {
var alertViews = mutableStateListOf<(@Composable () -> Unit)>() var alertViews = mutableStateListOf<(@Composable () -> Unit)>()
@@ -31,30 +35,39 @@ class AlertManager {
text: String? = null, text: String? = null,
buttons: @Composable () -> Unit, buttons: @Composable () -> Unit,
) { ) {
val alertText: (@Composable () -> Unit)? = if (text == null) null else { -> Text(text) }
showAlert { showAlert {
AlertDialog( AlertDialog(
onDismissRequest = this::hideAlert, onDismissRequest = this::hideAlert,
title = { Text(title) }, title = alertTitle(title),
text = alertText, text = alertText(text),
buttons = buttons buttons = buttons,
shape = RoundedCornerShape(corner = CornerSize(25.dp))
) )
} }
} }
fun showAlertDialogButtonsColumn( fun showAlertDialogButtonsColumn(
title: String, title: String,
text: String? = null, text: AnnotatedString? = null,
buttons: @Composable () -> Unit, buttons: @Composable () -> Unit,
) { ) {
showAlert { showAlert {
Dialog(onDismissRequest = this::hideAlert) { Dialog(onDismissRequest = this::hideAlert) {
Column(Modifier.background(MaterialTheme.colors.background)) { Column(
Text(title, Modifier.padding(DEFAULT_PADDING), fontSize = 18.sp) Modifier
if (text != null) { .background(MaterialTheme.colors.surface, RoundedCornerShape(corner = CornerSize(25.dp)))
Text(text) .padding(bottom = DEFAULT_PADDING)
} ) {
Text(
title,
Modifier.fillMaxWidth().padding(vertical = DEFAULT_PADDING),
textAlign = TextAlign.Center,
fontSize = 20.sp
)
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.high) { CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.high) {
if (text != null) {
Text(text, Modifier.fillMaxWidth().padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, bottom = DEFAULT_PADDING * 1.5f), fontSize = 16.sp, textAlign = TextAlign.Center, color = MaterialTheme.colors.secondary)
}
buttons() buttons()
} }
} }
@@ -72,24 +85,27 @@ class AlertManager {
onDismissRequest: (() -> Unit)? = null, onDismissRequest: (() -> Unit)? = null,
destructive: Boolean = false destructive: Boolean = false
) { ) {
val alertText: (@Composable () -> Unit)? = if (text == null) null else { -> Text(text) }
showAlert { showAlert {
AlertDialog( AlertDialog(
onDismissRequest = { onDismissRequest?.invoke(); hideAlert() }, onDismissRequest = { onDismissRequest?.invoke(); hideAlert() },
title = { Text(title) }, title = alertTitle(title),
text = alertText, text = alertText(text),
confirmButton = { buttons = {
TextButton(onClick = { Row (
onConfirm?.invoke() Modifier.fillMaxWidth().padding(horizontal = DEFAULT_PADDING).padding(bottom = DEFAULT_PADDING_HALF),
hideAlert() horizontalArrangement = Arrangement.SpaceBetween
}) { Text(confirmText, color = if (destructive) MaterialTheme.colors.error else Color.Unspecified) } ) {
TextButton(onClick = {
onDismiss?.invoke()
hideAlert()
}) { Text(dismissText) }
TextButton(onClick = {
onConfirm?.invoke()
hideAlert()
}) { Text(confirmText, color = if (destructive) MaterialTheme.colors.error else Color.Unspecified) }
}
}, },
dismissButton = { shape = RoundedCornerShape(corner = CornerSize(25.dp))
TextButton(onClick = {
onDismiss?.invoke()
hideAlert()
}) { Text(dismissText) }
}
) )
} }
} }
@@ -104,16 +120,15 @@ class AlertManager {
onDismissRequest: (() -> Unit)? = null, onDismissRequest: (() -> Unit)? = null,
destructive: Boolean = false destructive: Boolean = false
) { ) {
val alertText: (@Composable () -> Unit)? = if (text == null) null else { -> Text(text) }
showAlert { showAlert {
AlertDialog( AlertDialog(
onDismissRequest = { onDismissRequest?.invoke(); hideAlert() }, onDismissRequest = { onDismissRequest?.invoke(); hideAlert() },
title = { Text(title) }, title = alertTitle(title),
text = alertText, text = alertText(text),
buttons = { buttons = {
Column( Column(
Modifier.fillMaxWidth().padding(horizontal = 8.dp).padding(top = 16.dp, bottom = 2.dp), Modifier.fillMaxWidth().padding(horizontal = DEFAULT_PADDING_HALF).padding(top = DEFAULT_PADDING, bottom = 2.dp),
horizontalAlignment = Alignment.End horizontalAlignment = Alignment.CenterHorizontally
) { ) {
TextButton(onClick = { TextButton(onClick = {
onDismiss?.invoke() onDismiss?.invoke()
@@ -122,39 +137,42 @@ class AlertManager {
TextButton(onClick = { TextButton(onClick = {
onConfirm?.invoke() onConfirm?.invoke()
hideAlert() hideAlert()
}) { Text(confirmText, color = if (destructive) MaterialTheme.colors.error else Color.Unspecified) } }) { Text(confirmText, color = if (destructive) Color.Red else Color.Unspecified, textAlign = TextAlign.End) }
} }
}, },
shape = RoundedCornerShape(corner = CornerSize(25.dp))
) )
} }
} }
fun showAlertMsg( fun showAlertMsg(
title: String, text: String? = null, title: String, text: String? = null,
confirmText: String = generalGetString(R.string.ok), onConfirm: (() -> Unit)? = null confirmText: String = generalGetString(R.string.ok)
) { ) {
val alertText: (@Composable () -> Unit)? = if (text == null) null else { -> Text(text) }
showAlert { showAlert {
AlertDialog( AlertDialog(
onDismissRequest = this::hideAlert, onDismissRequest = this::hideAlert,
title = { Text(title) }, title = alertTitle(title),
text = alertText, text = alertText(text),
confirmButton = { buttons = {
TextButton(onClick = { Row(
onConfirm?.invoke() Modifier.fillMaxWidth().padding(horizontal = DEFAULT_PADDING).padding(bottom = DEFAULT_PADDING_HALF),
hideAlert() horizontalArrangement = Arrangement.Center
}) { Text(confirmText) } ) {
} TextButton(onClick = {
hideAlert()
}) { Text(confirmText, color = Color.Unspecified) }
}
},
shape = RoundedCornerShape(corner = CornerSize(25.dp))
) )
} }
} }
fun showAlertMsg( fun showAlertMsg(
title: Int, title: Int,
text: Int? = null, text: Int? = null,
confirmText: Int = R.string.ok, confirmText: Int = R.string.ok,
onConfirm: (() -> Unit)? = null ) = showAlertMsg(generalGetString(title), if (text != null) generalGetString(text) else null, generalGetString(confirmText))
) = showAlertMsg(generalGetString(title), if (text != null) generalGetString(text) else null, generalGetString(confirmText), onConfirm)
@Composable @Composable
fun showInView() { fun showInView() {
@@ -165,3 +183,30 @@ class AlertManager {
val shared = AlertManager() val shared = AlertManager()
} }
} }
private fun alertTitle(title: String): (@Composable () -> Unit)? {
return {
Text(
title,
Modifier.fillMaxWidth(),
textAlign = TextAlign.Center,
fontSize = 20.sp
)
}
}
private fun alertText(text: String?): (@Composable () -> Unit)? {
return if (text == null) {
null
} else {
({
Text(
text,
Modifier.fillMaxWidth(),
textAlign = TextAlign.Center,
fontSize = 16.sp,
color = MaterialTheme.colors.secondary
)
})
}
}
@@ -5,15 +5,12 @@ import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.Icon import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.*
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
@@ -23,18 +20,18 @@ import chat.simplex.app.model.ChatInfo
import chat.simplex.app.ui.theme.SimpleXTheme import chat.simplex.app.ui.theme.SimpleXTheme
@Composable @Composable
fun ChatInfoImage(chatInfo: ChatInfo, size: Dp, iconColor: Color = MaterialTheme.colors.secondary) { fun ChatInfoImage(chatInfo: ChatInfo, size: Dp, iconColor: Color = MaterialTheme.colors.secondaryVariant) {
val icon = val icon =
if (chatInfo is ChatInfo.Group) Icons.Filled.SupervisedUserCircle if (chatInfo is ChatInfo.Group) R.drawable.ic_supervised_user_circle_filled
else Icons.Filled.AccountCircle else R.drawable.ic_account_circle_filled
ProfileImage(size, chatInfo.image, icon, iconColor) ProfileImage(size, chatInfo.image, icon, iconColor)
} }
@Composable @Composable
fun IncognitoImage(size: Dp, iconColor: Color = MaterialTheme.colors.secondary) { fun IncognitoImage(size: Dp, iconColor: Color = MaterialTheme.colors.secondaryVariant) {
Box(Modifier.size(size)) { Box(Modifier.size(size)) {
Icon( Icon(
Icons.Filled.TheaterComedy, stringResource(R.string.incognito), painterResource(R.drawable.ic_theater_comedy_filled), stringResource(R.string.incognito),
modifier = Modifier.size(size).padding(size / 12), modifier = Modifier.size(size).padding(size / 12),
iconColor iconColor
) )
@@ -45,17 +42,31 @@ fun IncognitoImage(size: Dp, iconColor: Color = MaterialTheme.colors.secondary)
fun ProfileImage( fun ProfileImage(
size: Dp, size: Dp,
image: String? = null, image: String? = null,
icon: ImageVector = Icons.Filled.AccountCircle, icon: Int = R.drawable.ic_account_circle_filled,
color: Color = MaterialTheme.colors.secondary color: Color = MaterialTheme.colors.secondaryVariant
) { ) {
Box(Modifier.size(size)) { Box(Modifier.size(size)) {
if (image == null) { if (image == null) {
Icon( val iconToReplace = when (icon) {
icon, R.drawable.ic_account_circle_filled -> AccountCircleFilled
contentDescription = stringResource(R.string.icon_descr_profile_image_placeholder), R.drawable.ic_supervised_user_circle_filled -> SupervisedUserCircleFilled
tint = color, else -> null
modifier = Modifier.fillMaxSize() }
) if (iconToReplace != null) {
Icon(
iconToReplace,
contentDescription = stringResource(R.string.icon_descr_profile_image_placeholder),
tint = color,
modifier = Modifier.fillMaxSize()
)
} else {
Icon(
painterResource(icon),
contentDescription = stringResource(R.string.icon_descr_profile_image_placeholder),
tint = color,
modifier = Modifier.fillMaxSize()
)
}
} else { } else {
val imageBitmap = base64ToBitmap(image).asImageBitmap() val imageBitmap = base64ToBitmap(image).asImageBitmap()
Image( Image(
@@ -68,6 +79,7 @@ fun ProfileImage(
} }
} }
@Preview @Preview
@Composable @Composable
fun PreviewChatInfoImage() { fun PreviewChatInfoImage() {
@@ -1,21 +1,24 @@
package chat.simplex.app.views.helpers package chat.simplex.app.views.helpers
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.Icons import androidx.compose.material.MaterialTheme
import androidx.compose.material.icons.outlined.*
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import chat.simplex.app.R import chat.simplex.app.R
import chat.simplex.app.views.newchat.ActionButton import chat.simplex.app.views.newchat.ActionButton
sealed class AttachmentOption { sealed class AttachmentOption {
object TakePhoto: AttachmentOption() object CameraPhoto: AttachmentOption()
object PickImage: AttachmentOption() object GalleryImage: AttachmentOption()
object PickFile: AttachmentOption() object GalleryVideo: AttachmentOption()
object File: AttachmentOption()
} }
@Composable @Composable
@@ -37,16 +40,20 @@ fun ChooseAttachmentView(
.padding(horizontal = 8.dp, vertical = 30.dp), .padding(horizontal = 8.dp, vertical = 30.dp),
horizontalArrangement = Arrangement.SpaceEvenly horizontalArrangement = Arrangement.SpaceEvenly
) { ) {
ActionButton(null, stringResource(R.string.use_camera_button), icon = Icons.Outlined.PhotoCamera) { ActionButton(Modifier.fillMaxWidth(0.25f), null, stringResource(R.string.use_camera_button), icon = painterResource(R.drawable.ic_camera_enhance)) {
attachmentOption.value = AttachmentOption.TakePhoto attachmentOption.value = AttachmentOption.CameraPhoto
hide() hide()
} }
ActionButton(null, stringResource(R.string.from_gallery_button), icon = Icons.Outlined.Collections) { ActionButton(Modifier.fillMaxWidth(0.33f), null, stringResource(R.string.gallery_image_button), icon = painterResource(R.drawable.ic_add_photo)) {
attachmentOption.value = AttachmentOption.PickImage attachmentOption.value = AttachmentOption.GalleryImage
hide() hide()
} }
ActionButton(null, stringResource(R.string.choose_file), icon = Icons.Outlined.InsertDriveFile) { ActionButton(Modifier.fillMaxWidth(0.50f), null, stringResource(R.string.gallery_video_button), icon = painterResource(R.drawable.ic_smart_display)) {
attachmentOption.value = AttachmentOption.PickFile attachmentOption.value = AttachmentOption.GalleryVideo
hide()
}
ActionButton(Modifier.fillMaxWidth(1f), null, stringResource(R.string.choose_file), icon = painterResource(R.drawable.ic_note_add)) {
attachmentOption.value = AttachmentOption.File
hide() hide()
} }
} }
@@ -3,15 +3,20 @@ package chat.simplex.app.views.helpers
import android.content.res.Configuration import android.content.res.Configuration
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.material.* import androidx.compose.material.*
import androidx.compose.runtime.Composable import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import chat.simplex.app.ui.theme.* import chat.simplex.app.ui.theme.*
@Composable @Composable
fun CloseSheetBar(close: () -> Unit) { fun CloseSheetBar(close: (() -> Unit)?, endButtons: @Composable RowScope.() -> Unit = {}) {
Column( Column(
Modifier Modifier
.fillMaxWidth() .fillMaxWidth()
@@ -20,26 +25,40 @@ fun CloseSheetBar(close: () -> Unit) {
) { ) {
Row( Row(
Modifier Modifier
.width(TitleInsetWithIcon - AppBarHorizontalPadding)
.padding(top = 4.dp), // Like in DefaultAppBar .padding(top = 4.dp), // Like in DefaultAppBar
content = { NavigationButtonBack(close) } content = {
Row(
Modifier.fillMaxWidth().height(TextFieldDefaults.MinHeight),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
NavigationButtonBack(onButtonClicked = close)
Row {
endButtons()
}
}
}
) )
} }
} }
@Composable @Composable
fun AppBarTitle(title: String, withPadding: Boolean = true) { fun AppBarTitle(title: String, withPadding: Boolean = true) {
val padding = if (withPadding) val theme = CurrentColors.collectAsState()
PaddingValues(start = DEFAULT_PADDING, end = DEFAULT_PADDING, bottom = DEFAULT_PADDING) val titleColor = CurrentColors.collectAsState().value.appColors.title
else val brush = if (theme.value.base == DefaultTheme.SIMPLEX)
PaddingValues(bottom = DEFAULT_PADDING) Brush.linearGradient(listOf(titleColor.darker(0.2f), titleColor.lighter(0.35f)), Offset(0f, Float.POSITIVE_INFINITY), Offset(Float.POSITIVE_INFINITY, 0f))
else // color is not updated when changing themes if I pass null here
Brush.linearGradient(listOf(titleColor, titleColor), Offset(0f, Float.POSITIVE_INFINITY), Offset(Float.POSITIVE_INFINITY, 0f))
Text( Text(
title, title,
Modifier Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(padding), .padding(bottom = DEFAULT_PADDING * 1.5f, start = if (withPadding) DEFAULT_PADDING else 0.dp, end = if (withPadding) DEFAULT_PADDING else 0.dp,),
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.h1 style = MaterialTheme.typography.h1.copy(brush = brush),
color = MaterialTheme.colors.primaryVariant,
textAlign = TextAlign.Center
) )
} }
@@ -0,0 +1,143 @@
package chat.simplex.app.views.helpers
import androidx.compose.material.icons.materialIcon
import androidx.compose.material.icons.materialPath
import androidx.compose.ui.graphics.vector.*
val AccountCircleFilled: ImageVector
get() {
if (_accountCircleFilled != null) {
return _accountCircleFilled!!
}
_accountCircleFilled = materialIcon(name = "Filled.AccountCircle") {
materialPath {
moveTo(12.0f, 2.0f)
curveTo(6.48f, 2.0f, 2.0f, 6.48f, 2.0f, 12.0f)
reflectiveCurveToRelative(4.48f, 10.0f, 10.0f, 10.0f)
reflectiveCurveToRelative(10.0f, -4.48f, 10.0f, -10.0f)
reflectiveCurveTo(17.52f, 2.0f, 12.0f, 2.0f)
close()
moveTo(12.0f, 5.0f)
curveToRelative(1.66f, 0.0f, 3.0f, 1.34f, 3.0f, 3.0f)
reflectiveCurveToRelative(-1.34f, 3.0f, -3.0f, 3.0f)
reflectiveCurveToRelative(-3.0f, -1.34f, -3.0f, -3.0f)
reflectiveCurveToRelative(1.34f, -3.0f, 3.0f, -3.0f)
close()
moveTo(12.0f, 19.2f)
curveToRelative(-2.5f, 0.0f, -4.71f, -1.28f, -6.0f, -3.22f)
curveToRelative(0.03f, -1.99f, 4.0f, -3.08f, 6.0f, -3.08f)
curveToRelative(1.99f, 0.0f, 5.97f, 1.09f, 6.0f, 3.08f)
curveToRelative(-1.29f, 1.94f, -3.5f, 3.22f, -6.0f, 3.22f)
close()
}
}
return _accountCircleFilled!!
}
private var _accountCircleFilled: ImageVector? = null
val SupervisedUserCircleFilled: ImageVector
get() {
if (_supervisedUserCircleFilled != null) {
return _supervisedUserCircleFilled!!
}
_supervisedUserCircleFilled = materialIcon(name = "Filled.SupervisedUserCircle") {
materialPath {
moveTo(11.99f, 2.0f)
curveToRelative(-5.52f, 0.0f, -10.0f, 4.48f, -10.0f, 10.0f)
reflectiveCurveToRelative(4.48f, 10.0f, 10.0f, 10.0f)
reflectiveCurveToRelative(10.0f, -4.48f, 10.0f, -10.0f)
reflectiveCurveToRelative(-4.48f, -10.0f, -10.0f, -10.0f)
close()
moveTo(15.6f, 8.34f)
curveToRelative(1.07f, 0.0f, 1.93f, 0.86f, 1.93f, 1.93f)
curveToRelative(0.0f, 1.07f, -0.86f, 1.93f, -1.93f, 1.93f)
curveToRelative(-1.07f, 0.0f, -1.93f, -0.86f, -1.93f, -1.93f)
curveToRelative(-0.01f, -1.07f, 0.86f, -1.93f, 1.93f, -1.93f)
close()
moveTo(9.6f, 6.76f)
curveToRelative(1.3f, 0.0f, 2.36f, 1.06f, 2.36f, 2.36f)
curveToRelative(0.0f, 1.3f, -1.06f, 2.36f, -2.36f, 2.36f)
reflectiveCurveToRelative(-2.36f, -1.06f, -2.36f, -2.36f)
curveToRelative(0.0f, -1.31f, 1.05f, -2.36f, 2.36f, -2.36f)
close()
moveTo(9.6f, 15.89f)
verticalLineToRelative(3.75f)
curveToRelative(-2.4f, -0.75f, -4.3f, -2.6f, -5.14f, -4.96f)
curveToRelative(1.05f, -1.12f, 3.67f, -1.69f, 5.14f, -1.69f)
curveToRelative(0.53f, 0.0f, 1.2f, 0.08f, 1.9f, 0.22f)
curveToRelative(-1.64f, 0.87f, -1.9f, 2.02f, -1.9f, 2.68f)
close()
moveTo(11.99f, 20.0f)
curveToRelative(-0.27f, 0.0f, -0.53f, -0.01f, -0.79f, -0.04f)
verticalLineToRelative(-4.07f)
curveToRelative(0.0f, -1.42f, 2.94f, -2.13f, 4.4f, -2.13f)
curveToRelative(1.07f, 0.0f, 2.92f, 0.39f, 3.84f, 1.15f)
curveToRelative(-1.17f, 2.97f, -4.06f, 5.09f, -7.45f, 5.09f)
close()
}
}
return _supervisedUserCircleFilled!!
}
private var _supervisedUserCircleFilled: ImageVector? = null
val BoltFilled: ImageVector
get() {
if (_boltFilled != null) {
return _boltFilled!!
}
_boltFilled = materialIcon(name = "Filled.Bolt") {
materialPath {
moveTo(11.0f, 21.0f)
horizontalLineToRelative(-1.0f)
lineToRelative(1.0f, -7.0f)
horizontalLineTo(7.5f)
curveToRelative(-0.58f, 0.0f, -0.57f, -0.32f, -0.38f, -0.66f)
curveToRelative(0.19f, -0.34f, 0.05f, -0.08f, 0.07f, -0.12f)
curveTo(8.48f, 10.94f, 10.42f, 7.54f, 13.0f, 3.0f)
horizontalLineToRelative(1.0f)
lineToRelative(-1.0f, 7.0f)
horizontalLineToRelative(3.5f)
curveToRelative(0.49f, 0.0f, 0.56f, 0.33f, 0.47f, 0.51f)
lineToRelative(-0.07f, 0.15f)
curveTo(12.96f, 17.55f, 11.0f, 21.0f, 11.0f, 21.0f)
close()
}
}
return _boltFilled!!
}
private var _boltFilled: ImageVector? = null
val MoreVertFilled: ImageVector
get() {
if (_moreVertFilled != null) {
return _moreVertFilled!!
}
_moreVertFilled = materialIcon(name = "Filled.MoreVert") {
materialPath {
moveTo(12.0f, 8.0f)
curveToRelative(1.1f, 0.0f, 2.0f, -0.9f, 2.0f, -2.0f)
reflectiveCurveToRelative(-0.9f, -2.0f, -2.0f, -2.0f)
reflectiveCurveToRelative(-2.0f, 0.9f, -2.0f, 2.0f)
reflectiveCurveToRelative(0.9f, 2.0f, 2.0f, 2.0f)
close()
moveTo(12.0f, 10.0f)
curveToRelative(-1.1f, 0.0f, -2.0f, 0.9f, -2.0f, 2.0f)
reflectiveCurveToRelative(0.9f, 2.0f, 2.0f, 2.0f)
reflectiveCurveToRelative(2.0f, -0.9f, 2.0f, -2.0f)
reflectiveCurveToRelative(-0.9f, -2.0f, -2.0f, -2.0f)
close()
moveTo(12.0f, 16.0f)
curveToRelative(-1.1f, 0.0f, -2.0f, 0.9f, -2.0f, 2.0f)
reflectiveCurveToRelative(0.9f, 2.0f, 2.0f, 2.0f)
reflectiveCurveToRelative(2.0f, -0.9f, 2.0f, -2.0f)
reflectiveCurveToRelative(-0.9f, -2.0f, -2.0f, -2.0f)
close()
}
}
return _moreVertFilled!!
}
private var _moreVertFilled: ImageVector? = null
@@ -3,6 +3,7 @@ package chat.simplex.app.views.helpers
import android.util.Log import android.util.Log
import chat.simplex.app.* import chat.simplex.app.*
import chat.simplex.app.model.AppPreferences import chat.simplex.app.model.AppPreferences
import chat.simplex.app.model.SharedPreference
import chat.simplex.app.views.usersettings.Cryptor import chat.simplex.app.views.usersettings.Cryptor
import kotlinx.serialization.* import kotlinx.serialization.*
import java.io.File import java.io.File
@@ -16,30 +17,36 @@ object DatabaseUtils {
} }
private const val DATABASE_PASSWORD_ALIAS: String = "databasePassword" private const val DATABASE_PASSWORD_ALIAS: String = "databasePassword"
private const val APP_PASSWORD_ALIAS: String = "appPassword"
val ksDatabasePassword = KeyStoreItem(DATABASE_PASSWORD_ALIAS, appPreferences.encryptedDBPassphrase, appPreferences.initializationVectorDBPassphrase)
val ksAppPassword = KeyStoreItem(APP_PASSWORD_ALIAS, appPreferences.encryptedAppPassphrase, appPreferences.initializationVectorAppPassphrase)
class KeyStoreItem(private val alias: String, val passphrase: SharedPreference<String?>, val initVector: SharedPreference<String?>) {
fun get(): String? {
return cryptor.decryptData(
passphrase.get()?.toByteArrayFromBase64() ?: return null,
initVector.get()?.toByteArrayFromBase64() ?: return null,
alias,
)
}
fun set(key: String) {
val data = cryptor.encryptText(key, alias)
passphrase.set(data.first.toBase64String())
initVector.set(data.second.toBase64String())
}
fun remove() {
cryptor.deleteKey(alias)
passphrase.set(null)
initVector.set(null)
}
}
private fun hasDatabase(rootDir: String): Boolean = private fun hasDatabase(rootDir: String): Boolean =
File(rootDir + File.separator + "files_chat.db").exists() && File(rootDir + File.separator + "files_agent.db").exists() File(rootDir + File.separator + "files_chat.db").exists() && File(rootDir + File.separator + "files_agent.db").exists()
fun getDatabaseKey(): String? {
return cryptor.decryptData(
appPreferences.encryptedDBPassphrase.get()?.toByteArrayFromBase64() ?: return null,
appPreferences.initializationVectorDBPassphrase.get()?.toByteArrayFromBase64() ?: return null,
DATABASE_PASSWORD_ALIAS,
)
}
fun setDatabaseKey(key: String) {
val data = cryptor.encryptText(key, DATABASE_PASSWORD_ALIAS)
appPreferences.encryptedDBPassphrase.set(data.first.toBase64String())
appPreferences.initializationVectorDBPassphrase.set(data.second.toBase64String())
}
fun removeDatabaseKey() {
cryptor.deleteKey(DATABASE_PASSWORD_ALIAS)
appPreferences.encryptedDBPassphrase.set(null)
appPreferences.initializationVectorDBPassphrase.set(null)
}
fun useDatabaseKey(): String { fun useDatabaseKey(): String {
Log.d(TAG, "useDatabaseKey ${appPreferences.storeDBPassphrase.get()}") Log.d(TAG, "useDatabaseKey ${appPreferences.storeDBPassphrase.get()}")
var dbKey = "" var dbKey = ""
@@ -47,10 +54,10 @@ object DatabaseUtils {
if (useKeychain) { if (useKeychain) {
if (!hasDatabase(SimplexApp.context.dataDir.absolutePath)) { if (!hasDatabase(SimplexApp.context.dataDir.absolutePath)) {
dbKey = randomDatabasePassword() dbKey = randomDatabasePassword()
setDatabaseKey(dbKey) ksDatabasePassword.set(dbKey)
appPreferences.initialRandomDBPassphrase.set(true) appPreferences.initialRandomDBPassphrase.set(true)
} else { } else {
dbKey = getDatabaseKey() ?: "" dbKey = ksDatabasePassword.get() ?: ""
} }
} }
return dbKey return dbKey
@@ -66,8 +73,39 @@ object DatabaseUtils {
@Serializable @Serializable
sealed class DBMigrationResult { sealed class DBMigrationResult {
@Serializable @SerialName("ok") object OK: DBMigrationResult() @Serializable @SerialName("ok") object OK: DBMigrationResult()
@Serializable @SerialName("invalidConfirmation") object InvalidConfirmation: DBMigrationResult()
@Serializable @SerialName("errorNotADatabase") class ErrorNotADatabase(val dbFile: String): DBMigrationResult() @Serializable @SerialName("errorNotADatabase") class ErrorNotADatabase(val dbFile: String): DBMigrationResult()
@Serializable @SerialName("error") class Error(val dbFile: String, val migrationError: String): DBMigrationResult() @Serializable @SerialName("errorMigration") class ErrorMigration(val dbFile: String, val migrationError: MigrationError): DBMigrationResult()
@Serializable @SerialName("errorSQL") class ErrorSQL(val dbFile: String, val migrationSQLError: String): DBMigrationResult()
@Serializable @SerialName("errorKeychain") object ErrorKeychain: DBMigrationResult() @Serializable @SerialName("errorKeychain") object ErrorKeychain: DBMigrationResult()
@Serializable @SerialName("unknown") class Unknown(val json: String): DBMigrationResult() @Serializable @SerialName("unknown") class Unknown(val json: String): DBMigrationResult()
} }
enum class MigrationConfirmation(val value: String) {
YesUp("yesUp"),
YesUpDown ("yesUpDown"),
Error("error")
}
fun defaultMigrationConfirmation(appPrefs: AppPreferences): MigrationConfirmation =
if (appPrefs.confirmDBUpgrades.get()) MigrationConfirmation.Error else MigrationConfirmation.YesUp
@Serializable
sealed class MigrationError {
@Serializable @SerialName("upgrade") class Upgrade(val upMigrations: List<UpMigration>): MigrationError()
@Serializable @SerialName("downgrade") class Downgrade(val downMigrations: List<String>): MigrationError()
@Serializable @SerialName("migrationError") class Error(val mtrError: MTRError): MigrationError()
}
@Serializable
data class UpMigration(
val upName: String,
// val withDown: Boolean
)
@Serializable
sealed class MTRError {
@Serializable @SerialName("noDown") class NoDown(val dbMigrations: List<String>): MTRError()
@Serializable @SerialName("different") class Different(val appMigration: String, val dbMigration: String): MTRError()
}
@@ -3,6 +3,7 @@ package chat.simplex.app.views.helpers
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.shape.ZeroCornerSize import androidx.compose.foundation.shape.ZeroCornerSize
import androidx.compose.foundation.text.* import androidx.compose.foundation.text.*
import androidx.compose.material.* import androidx.compose.material.*
@@ -14,13 +15,19 @@ import androidx.compose.ui.focus.*
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.text.TextRange import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.*
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.* import androidx.compose.ui.text.input.*
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import chat.simplex.app.R
import chat.simplex.app.views.database.PassphraseStrength
import chat.simplex.app.views.database.validKey
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.launch
@OptIn(ExperimentalComposeUiApi::class) @OptIn(ExperimentalComposeUiApi::class)
@Composable @Composable
@@ -110,3 +117,109 @@ fun DefaultBasicTextField(
} }
) )
} }
@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun DefaultConfigurableTextField(
state: MutableState<TextFieldValue>,
placeholder: String,
modifier: Modifier = Modifier,
showPasswordStrength: Boolean = false,
isValid: (String) -> Boolean,
keyboardActions: KeyboardActions = KeyboardActions(),
keyboardType: KeyboardType = KeyboardType.Text,
dependsOn: State<Any?>? = null,
) {
var valid by remember { mutableStateOf(validKey(state.value.text)) }
var showKey by remember { mutableStateOf(false) }
val icon = if (valid) {
if (showKey) painterResource(R.drawable.ic_visibility_off_filled) else painterResource(R.drawable.ic_visibility_filled)
} else painterResource(R.drawable.ic_error)
val iconColor = if (valid) {
if (showPasswordStrength && state.value.text.isNotEmpty()) PassphraseStrength.check(state.value.text).color else MaterialTheme.colors.secondary
} else Color.Red
val keyboard = LocalSoftwareKeyboardController.current
val keyboardOptions = KeyboardOptions(
imeAction = if (keyboardActions.onNext != null) ImeAction.Next else ImeAction.Done,
autoCorrect = keyboardType != KeyboardType.Password,
keyboardType = keyboardType
)
val enabled = true
val colors = TextFieldDefaults.textFieldColors(
backgroundColor = Color.Unspecified,
textColor = MaterialTheme.colors.onBackground,
focusedIndicatorColor = Color.Unspecified,
unfocusedIndicatorColor = Color.Unspecified,
)
val color = MaterialTheme.colors.onBackground
val shape = MaterialTheme.shapes.small.copy(bottomEnd = ZeroCornerSize, bottomStart = ZeroCornerSize)
val interactionSource = remember { MutableInteractionSource() }
BasicTextField(
value = state.value,
modifier = modifier
.fillMaxWidth()
.background(colors.backgroundColor(enabled).value, shape)
.indicatorLine(enabled, false, interactionSource, colors)
.defaultMinSize(
minWidth = TextFieldDefaults.MinWidth,
minHeight = TextFieldDefaults.MinHeight
),
onValueChange = {
state.value = it
},
cursorBrush = SolidColor(colors.cursorColor(false).value),
visualTransformation = if (showKey || keyboardType != KeyboardType.Password)
VisualTransformation.None
else
VisualTransformation { TransformedText(AnnotatedString(it.text.map { "*" }.joinToString(separator = "")), OffsetMapping.Identity) },
keyboardOptions = keyboardOptions,
keyboardActions = KeyboardActions(onDone = {
keyboard?.hide()
keyboardActions.onDone?.invoke(this)
}),
singleLine = true,
textStyle = TextStyle.Default.copy(
color = color,
fontWeight = FontWeight.Normal,
fontSize = 16.sp
),
interactionSource = interactionSource,
decorationBox = @Composable { innerTextField ->
TextFieldDefaults.TextFieldDecorationBox(
value = state.value.text,
innerTextField = innerTextField,
placeholder = { Text(placeholder, color = MaterialTheme.colors.secondary) },
singleLine = true,
enabled = enabled,
isError = !valid,
trailingIcon = {
if (keyboardType == KeyboardType.Password || !valid) {
IconButton({ showKey = !showKey }) {
Icon(icon, null, tint = iconColor)
}
}
},
interactionSource = interactionSource,
contentPadding = TextFieldDefaults.textFieldWithLabelPadding(start = 0.dp, end = 0.dp),
visualTransformation = VisualTransformation.None,
colors = colors
)
}
)
LaunchedEffect(Unit) {
launch {
snapshotFlow { state.value }
.distinctUntilChanged()
.collect {
valid = isValid(it.text)
}
}
launch {
snapshotFlow { dependsOn?.value }
.distinctUntilChanged()
.collect {
valid = isValid(state.value.text)
}
}
}
}
@@ -0,0 +1,61 @@
package chat.simplex.app.views.helpers
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CornerSize
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.dp
import chat.simplex.app.ui.theme.*
@Composable
fun DefaultDropdownMenu(
showMenu: MutableState<Boolean>,
offset: DpOffset = DpOffset(0.dp, 0.dp),
dropdownMenuItems: (@Composable () -> Unit)?
) {
MaterialTheme(
shapes = MaterialTheme.shapes.copy(medium = RoundedCornerShape(corner = CornerSize(25.dp)))
) {
DropdownMenu(
expanded = showMenu.value,
onDismissRequest = { showMenu.value = false },
Modifier
.widthIn(min = 250.dp)
.background(MaterialTheme.colors.surface)
.padding(vertical = 4.dp),
offset = offset,
) {
dropdownMenuItems?.invoke()
}
}
}
@Composable
fun ExposedDropdownMenuBoxScope.DefaultExposedDropdownMenu(
expanded: MutableState<Boolean>,
modifier: Modifier = Modifier,
dropdownMenuItems: (@Composable () -> Unit)?
) {
MaterialTheme(
shapes = MaterialTheme.shapes.copy(medium = RoundedCornerShape(corner = CornerSize(25.dp)))
) {
ExposedDropdownMenu(
modifier = Modifier
.widthIn(min = 200.dp)
.background(MaterialTheme.colors.surface)
.then(modifier),
expanded = expanded.value,
onDismissRequest = {
expanded.value = false
}
) {
dropdownMenuItems?.invoke()
}
}
}
@@ -0,0 +1,39 @@
package chat.simplex.app.views.helpers
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.material.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.geometry.*
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.dp
@Composable
fun DefaultSwitch(
checked: Boolean,
onCheckedChange: ((Boolean) -> Unit)?,
modifier: Modifier = Modifier,
enabled: Boolean = true,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
colors: SwitchColors = SwitchDefaults.colors(
checkedThumbColor = MaterialTheme.colors.primary,
uncheckedThumbColor = MaterialTheme.colors.secondary,
checkedTrackAlpha = 0.0f,
uncheckedTrackAlpha = 0.0f,
)
) {
val color = if (checked) MaterialTheme.colors.primary.copy(alpha = 0.3f) else MaterialTheme.colors.secondary.copy(alpha = 0.3f)
val size = with(LocalDensity.current) { Size(46.dp.toPx(), 28.dp.toPx()) }
val offset = with(LocalDensity.current) { Offset(1.dp.toPx(), 10.dp.toPx()) }
val radius = with(LocalDensity.current) { 28.dp.toPx() }
Switch(
checked = checked,
onCheckedChange = onCheckedChange,
modifier.drawBehind { drawRoundRect(color, size = size, topLeft = offset, cornerRadius = CornerRadius(radius, radius)) },
colors = colors,
enabled = enabled,
interactionSource = interactionSource,
)
}
@@ -4,12 +4,11 @@ import chat.simplex.app.R
import androidx.compose.foundation.* import androidx.compose.foundation.*
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.material.* import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.*
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.* import androidx.compose.ui.graphics.*
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import chat.simplex.app.ui.theme.* import chat.simplex.app.ui.theme.*
@@ -34,7 +33,7 @@ fun DefaultTopAppBar(
if (!showSearch) { if (!showSearch) {
title?.invoke() title?.invoke()
} else { } else {
SearchTextField(Modifier.fillMaxWidth(), stringResource(android.R.string.search_go), onSearchValueChanged) SearchTextField(Modifier.fillMaxWidth(), stringResource(android.R.string.search_go), alwaysVisible = false, onSearchValueChanged)
} }
}, },
backgroundColor = if (isInDarkTheme()) ToolbarDark else ToolbarLight, backgroundColor = if (isInDarkTheme()) ToolbarDark else ToolbarLight,
@@ -45,10 +44,19 @@ fun DefaultTopAppBar(
} }
@Composable @Composable
fun NavigationButtonBack(onButtonClicked: () -> Unit) { fun NavigationButtonBack(onButtonClicked: (() -> Unit)?) {
IconButton(onButtonClicked ?: {}, enabled = onButtonClicked != null) {
Icon(
painterResource(R.drawable.ic_arrow_back_ios_new), stringResource(R.string.back), tint = if (onButtonClicked != null) MaterialTheme.colors.primary else MaterialTheme.colors.secondary
)
}
}
@Composable
fun ShareButton(onButtonClicked: () -> Unit) {
IconButton(onButtonClicked) { IconButton(onButtonClicked) {
Icon( Icon(
Icons.Outlined.ArrowBackIos, stringResource(R.string.back), tint = MaterialTheme.colors.primary painterResource(R.drawable.ic_share), stringResource(R.string.share_verb), tint = MaterialTheme.colors.primary
) )
} }
} }
@@ -57,7 +65,7 @@ fun NavigationButtonBack(onButtonClicked: () -> Unit) {
fun NavigationButtonMenu(onButtonClicked: () -> Unit) { fun NavigationButtonMenu(onButtonClicked: () -> Unit) {
IconButton(onClick = onButtonClicked) { IconButton(onClick = onButtonClicked) {
Icon( Icon(
Icons.Outlined.Menu, painterResource(R.drawable.ic_menu),
stringResource(R.string.icon_descr_settings), stringResource(R.string.icon_descr_settings),
tint = MaterialTheme.colors.primary, tint = MaterialTheme.colors.primary,
) )
@@ -1,17 +1,21 @@
@file:UseSerializers(UriSerializer::class)
package chat.simplex.app.views.helpers package chat.simplex.app.views.helpers
import android.graphics.Bitmap
import android.net.Uri import android.net.Uri
import androidx.compose.runtime.saveable.Saver import androidx.compose.runtime.saveable.Saver
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.serialization.*
import kotlinx.serialization.descriptors.*
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
sealed class SharedContent { sealed class SharedContent {
data class Text(val text: String): SharedContent() data class Text(val text: String): SharedContent()
data class Images(val text: String, val uris: List<Uri>): SharedContent() data class Media(val text: String, val uris: List<Uri>): SharedContent()
data class File(val text: String, val uri: Uri): SharedContent() data class File(val text: String, val uri: Uri): SharedContent()
} }
enum class NewChatSheetState { enum class AnimatedViewState {
VISIBLE, HIDING, GONE; VISIBLE, HIDING, GONE;
fun isVisible(): Boolean { fun isVisible(): Boolean {
return this == VISIBLE return this == VISIBLE
@@ -23,7 +27,7 @@ enum class NewChatSheetState {
return this == GONE return this == GONE
} }
companion object { companion object {
fun saver(): Saver<MutableStateFlow<NewChatSheetState>, *> = Saver( fun saver(): Saver<MutableStateFlow<AnimatedViewState>, *> = Saver(
save = { it.value.toString() }, save = { it.value.toString() },
restore = { restore = {
MutableStateFlow(valueOf(it)) MutableStateFlow(valueOf(it))
@@ -32,7 +36,17 @@ enum class NewChatSheetState {
} }
} }
sealed class UploadContent {
data class SimpleImage(val uri: Uri): UploadContent() @Serializer(forClass = Uri::class)
data class AnimatedImage(val uri: Uri): UploadContent() object UriSerializer : KSerializer<Uri> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("Uri", PrimitiveKind.STRING)
override fun serialize(encoder: Encoder, value: Uri) = encoder.encodeString(value.toString())
override fun deserialize(decoder: Decoder): Uri = Uri.parse(decoder.decodeString())
}
@Serializable
sealed class UploadContent {
@Serializable data class SimpleImage(val uri: Uri): UploadContent()
@Serializable data class AnimatedImage(val uri: Uri): UploadContent()
@Serializable data class Video(val uri: Uri, val duration: Int): UploadContent()
} }
@@ -2,21 +2,19 @@ package chat.simplex.app.views.helpers
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.material.* import androidx.compose.material.*
import androidx.compose.material.icons.Icons import androidx.compose.ui.res.painterResource
import androidx.compose.material.icons.outlined.ExpandLess
import androidx.compose.material.icons.outlined.ExpandMore
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import chat.simplex.app.R import chat.simplex.app.R
import chat.simplex.app.ui.theme.DEFAULT_PADDING import chat.simplex.app.ui.theme.*
import chat.simplex.app.ui.theme.HighOrLowlight import chat.simplex.app.views.usersettings.SettingsActionItemWithContent
@Composable @Composable
fun <T> ExposedDropDownSettingRow( fun <T> ExposedDropDownSettingRow(
@@ -24,31 +22,17 @@ fun <T> ExposedDropDownSettingRow(
values: List<Pair<T, String>>, values: List<Pair<T, String>>,
selection: State<T>, selection: State<T>,
label: String? = null, label: String? = null,
icon: ImageVector? = null, icon: Painter? = null,
iconTint: Color = HighOrLowlight, iconTint: Color = MaterialTheme.colors.secondary,
enabled: State<Boolean> = mutableStateOf(true), enabled: State<Boolean> = mutableStateOf(true),
onSelected: (T) -> Unit onSelected: (T) -> Unit
) { ) {
Row( SettingsActionItemWithContent(icon, title, iconColor = iconTint, disabled = !enabled.value) {
Modifier.fillMaxWidth().padding(vertical = 10.dp), val expanded = remember { mutableStateOf(false) }
verticalAlignment = Alignment.CenterVertically,
) {
var expanded by remember { mutableStateOf(false) }
if (icon != null) {
Icon(
icon,
"",
Modifier.padding(end = 8.dp),
tint = iconTint
)
}
Text(title, Modifier.weight(1f), color = if (enabled.value) Color.Unspecified else HighOrLowlight)
ExposedDropdownMenuBox( ExposedDropdownMenuBox(
expanded = expanded, expanded = expanded.value,
onExpandedChange = { onExpandedChange = {
expanded = !expanded && enabled.value expanded.value = !expanded.value && enabled.value
} }
) { ) {
Row( Row(
@@ -56,39 +40,38 @@ fun <T> ExposedDropDownSettingRow(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.End horizontalArrangement = Arrangement.End
) { ) {
val maxWidth = with(LocalDensity.current){ 180.sp.toDp() } val maxWidth = with(LocalDensity.current) { 180.sp.toDp() }
Text( Text(
values.first { it.first == selection.value }.second + (if (label != null) " $label" else ""), values.first { it.first == selection.value }.second + (if (label != null) " $label" else ""),
Modifier.widthIn(max = maxWidth), Modifier.widthIn(max = maxWidth),
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
color = HighOrLowlight color = MaterialTheme.colors.secondary
) )
Spacer(Modifier.size(12.dp)) Spacer(Modifier.size(12.dp))
Icon( Icon(
if (!expanded) Icons.Outlined.ExpandMore else Icons.Outlined.ExpandLess, if (!expanded.value) painterResource(R.drawable.ic_expand_more) else painterResource(R.drawable.ic_expand_less),
generalGetString(R.string.icon_descr_more_button), generalGetString(R.string.icon_descr_more_button),
tint = HighOrLowlight tint = MaterialTheme.colors.secondary
) )
} }
ExposedDropdownMenu( DefaultExposedDropdownMenu(
modifier = Modifier.widthIn(min = 200.dp), modifier = Modifier.widthIn(min = 200.dp),
expanded = expanded, expanded = expanded,
onDismissRequest = {
expanded = false
}
) { ) {
values.forEach { selectionOption -> values.forEach { selectionOption ->
DropdownMenuItem( DropdownMenuItem(
onClick = { onClick = {
onSelected(selectionOption.first) onSelected(selectionOption.first)
expanded = false expanded.value = false
} },
contentPadding = PaddingValues(horizontal = DEFAULT_PADDING * 1.5f)
) { ) {
Text( Text(
selectionOption.second + (if (label != null) " $label" else ""), selectionOption.second + (if (label != null) " $label" else ""),
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
color = if (isInDarkTheme()) MenuTextColorDark else Color.Black,
) )
} }
} }
@@ -80,10 +80,16 @@ suspend fun PointerInputScope.detectGesture(
pressScope.release() pressScope.release()
} }
} catch (_: PointerEventTimeoutCancellationException) { } catch (_: PointerEventTimeoutCancellationException) {
onLongPress?.invoke(down.position) if (onLongPress != null) {
if (shouldConsume) onLongPress(down.position)
consumeUntilUp() if (shouldConsume)
pressScope.release() consumeUntilUp()
pressScope.cancel()
} else {
if (shouldConsume)
consumeUntilUp()
pressScope.release()
}
} }
} }
} }
@@ -17,15 +17,13 @@ import androidx.activity.result.contract.ActivityResultContract
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.CallSuper import androidx.annotation.CallSuper
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Collections
import androidx.compose.material.icons.outlined.PhotoCamera
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.Saver import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
@@ -113,7 +111,7 @@ fun base64ToBitmap(base64ImageString: String): Bitmap {
class CustomTakePicturePreview(var uri: Uri?, var tmpFile: File?): ActivityResultContract<Void?, Uri?>() { class CustomTakePicturePreview(var uri: Uri?, var tmpFile: File?): ActivityResultContract<Void?, Uri?>() {
@CallSuper @CallSuper
override fun createIntent(context: Context, input: Void?): Intent { override fun createIntent(context: Context, input: Void?): Intent {
tmpFile = File.createTempFile("image", ".bmp", context.filesDir) tmpFile = File.createTempFile("image", ".bmp", File(getAppFilesDirectory(SimplexApp.context)))
// Since the class should return Uri, the file should be deleted somewhere else. And in order to be sure, delegate this to system // Since the class should return Uri, the file should be deleted somewhere else. And in order to be sure, delegate this to system
tmpFile?.deleteOnExit() tmpFile?.deleteOnExit()
uri = FileProvider.getUriForFile(context, "${BuildConfig.APPLICATION_ID}.provider", tmpFile!!) uri = FileProvider.getUriForFile(context, "${BuildConfig.APPLICATION_ID}.provider", tmpFile!!)
@@ -174,7 +172,18 @@ fun rememberGetContentLauncher(cb: (Uri?) -> Unit): ManagedActivityResultLaunche
@Composable @Composable
fun rememberGetMultipleContentsLauncher(cb: (List<Uri>) -> Unit): ManagedActivityResultLauncher<String, List<Uri>> = fun rememberGetMultipleContentsLauncher(cb: (List<Uri>) -> Unit): ManagedActivityResultLauncher<String, List<Uri>> =
rememberLauncherForActivityResult(contract = ActivityResultContracts.GetMultipleContents(), cb) rememberLauncherForActivityResult(contract = GetMultipleContentsAndMimeTypes(), cb)
class GetMultipleContentsAndMimeTypes: ActivityResultContracts.GetMultipleContents() {
override fun createIntent(context: Context, input: String): Intent {
val mimeTypes = input.split(";")
return super.createIntent(context, mimeTypes[0]).apply {
if (mimeTypes.isNotEmpty()) {
putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes.toTypedArray())
}
}
}
}
fun ManagedActivityResultLauncher<Void?, Uri?>.launchWithFallback() { fun ManagedActivityResultLauncher<Void?, Uri?>.launchWithFallback() {
try { try {
@@ -204,22 +213,16 @@ fun GetImageBottomSheet(
val context = LocalContext.current val context = LocalContext.current
val processPickedImage = { uri: Uri? -> val processPickedImage = { uri: Uri? ->
if (uri != null) { if (uri != null) {
val source = ImageDecoder.createSource(context.contentResolver, uri) val bitmap = getBitmapFromUri(uri)
val bitmap = ImageDecoder.decodeBitmap(source) if (bitmap != null) {
imageBitmap.value = uri imageBitmap.value = uri
onImageChange(bitmap) onImageChange(bitmap)
} }
}
val galleryLauncher = rememberLauncherForActivityResult(contract = PickFromGallery()) { processPickedImage(it) }
val galleryLauncherFallback = rememberGetContentLauncher { processPickedImage(it) }
val cameraLauncher = rememberCameraLauncher { uri: Uri? ->
if (uri != null) {
imageBitmap.value = uri
val source = ImageDecoder.createSource(SimplexApp.context.contentResolver, uri)
val bitmap = ImageDecoder.decodeBitmap(source)
onImageChange(bitmap)
} }
} }
val galleryLauncher = rememberLauncherForActivityResult(contract = PickFromGallery(), processPickedImage)
val galleryLauncherFallback = rememberGetContentLauncher(processPickedImage)
val cameraLauncher = rememberCameraLauncher(processPickedImage)
val permissionLauncher = rememberPermissionLauncher { isGranted: Boolean -> val permissionLauncher = rememberPermissionLauncher { isGranted: Boolean ->
if (isGranted) { if (isGranted) {
cameraLauncher.launchWithFallback() cameraLauncher.launchWithFallback()
@@ -243,7 +246,7 @@ fun GetImageBottomSheet(
.padding(horizontal = 8.dp, vertical = 30.dp), .padding(horizontal = 8.dp, vertical = 30.dp),
horizontalArrangement = Arrangement.SpaceEvenly horizontalArrangement = Arrangement.SpaceEvenly
) { ) {
ActionButton(null, stringResource(R.string.use_camera_button), icon = Icons.Outlined.PhotoCamera) { ActionButton(null, stringResource(R.string.use_camera_button), icon = painterResource(R.drawable.ic_photo_camera)) {
when (PackageManager.PERMISSION_GRANTED) { when (PackageManager.PERMISSION_GRANTED) {
ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) -> { ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) -> {
cameraLauncher.launchWithFallback() cameraLauncher.launchWithFallback()
@@ -254,7 +257,7 @@ fun GetImageBottomSheet(
} }
} }
} }
ActionButton(null, stringResource(R.string.from_gallery_button), icon = Icons.Outlined.Collections) { ActionButton(null, stringResource(R.string.from_gallery_button), icon = painterResource(R.drawable.ic_image)) {
try { try {
galleryLauncher.launch(0) galleryLauncher.launch(0)
} catch (e: ActivityNotFoundException) { } catch (e: ActivityNotFoundException) {
@@ -6,13 +6,13 @@ import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.material.* import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Close
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
@@ -20,9 +20,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import chat.simplex.app.R import chat.simplex.app.R
import chat.simplex.app.model.LinkPreview import chat.simplex.app.model.LinkPreview
import chat.simplex.app.ui.theme.HighOrLowlight import chat.simplex.app.ui.theme.*
import chat.simplex.app.ui.theme.SimpleXTheme
import chat.simplex.app.views.chat.item.SentColorLight
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.jsoup.Jsoup import org.jsoup.Jsoup
@@ -80,8 +78,9 @@ suspend fun getLinkPreview(url: String): LinkPreview? {
@Composable @Composable
fun ComposeLinkView(linkPreview: LinkPreview?, cancelPreview: () -> Unit) { fun ComposeLinkView(linkPreview: LinkPreview?, cancelPreview: () -> Unit) {
val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage
Row( Row(
Modifier.fillMaxWidth().padding(top = 8.dp).background(SentColorLight), Modifier.fillMaxWidth().padding(top = 8.dp).background(sentColor),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
if (linkPreview == null) { if (linkPreview == null) {
@@ -91,7 +90,7 @@ fun ComposeLinkView(linkPreview: LinkPreview?, cancelPreview: () -> Unit) {
) { ) {
CircularProgressIndicator( CircularProgressIndicator(
Modifier.size(16.dp), Modifier.size(16.dp),
color = HighOrLowlight, color = MaterialTheme.colors.secondary,
strokeWidth = 2.dp strokeWidth = 2.dp
) )
} }
@@ -112,7 +111,7 @@ fun ComposeLinkView(linkPreview: LinkPreview?, cancelPreview: () -> Unit) {
} }
IconButton(onClick = cancelPreview, modifier = Modifier.padding(0.dp)) { IconButton(onClick = cancelPreview, modifier = Modifier.padding(0.dp)) {
Icon( Icon(
Icons.Outlined.Close, painterResource(R.drawable.ic_close),
contentDescription = stringResource(R.string.icon_descr_cancel_link_preview), contentDescription = stringResource(R.string.icon_descr_cancel_link_preview),
tint = MaterialTheme.colors.primary, tint = MaterialTheme.colors.primary,
modifier = Modifier.padding(10.dp) modifier = Modifier.padding(10.dp)
@@ -135,7 +134,7 @@ fun ChatItemLinkView(linkPreview: LinkPreview) {
if (linkPreview.description != "") { if (linkPreview.description != "") {
Text(linkPreview.description, maxLines = 12, overflow = TextOverflow.Ellipsis, fontSize = 14.sp, lineHeight = 20.sp) Text(linkPreview.description, maxLines = 12, overflow = TextOverflow.Ellipsis, fontSize = 14.sp, lineHeight = 20.sp)
} }
Text(linkPreview.uri, maxLines = 1, overflow = TextOverflow.Ellipsis, fontSize = 12.sp, color = HighOrLowlight) Text(linkPreview.uri, maxLines = 1, overflow = TextOverflow.Ellipsis, fontSize = 12.sp, color = MaterialTheme.colors.secondary)
} }
} }
} }
@@ -1,36 +1,71 @@
package chat.simplex.app.views.helpers package chat.simplex.app.views.helpers
import android.content.Context
import android.os.Build.VERSION.SDK_INT import android.os.Build.VERSION.SDK_INT
import android.widget.Toast import androidx.activity.compose.BackHandler
import androidx.biometric.BiometricManager import androidx.biometric.BiometricManager
import androidx.biometric.BiometricManager.Authenticators.* import androidx.biometric.BiometricManager.Authenticators.*
import androidx.biometric.BiometricPrompt import androidx.biometric.BiometricPrompt
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.ui.Modifier
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import chat.simplex.app.R import chat.simplex.app.R
import chat.simplex.app.SimplexApp
import chat.simplex.app.views.helpers.DatabaseUtils.ksAppPassword
import chat.simplex.app.views.localauth.LocalAuthView
import chat.simplex.app.views.usersettings.LAMode
sealed class LAResult { sealed class LAResult {
object Success: LAResult() object Success: LAResult()
class Error(val errString: CharSequence): LAResult() class Error(val errString: CharSequence): LAResult()
object Failed: LAResult() class Failed(val errString: CharSequence? = null): LAResult()
object Unavailable: LAResult() class Unavailable(val errString: CharSequence? = null): LAResult()
}
data class LocalAuthRequest (
val title: String?,
val reason: String,
val password: String,
val completed: (LAResult) -> Unit
) {
companion object {
val sample = LocalAuthRequest(generalGetString(R.string.la_enter_app_passcode), generalGetString(R.string.la_authenticate), "") { }
}
} }
fun authenticate( fun authenticate(
promptTitle: String, promptTitle: String,
promptSubtitle: String, promptSubtitle: String,
activity: FragmentActivity, activity: FragmentActivity,
usingLAMode: LAMode = SimplexApp.context.chatModel.controller.appPrefs.laMode.get(),
completed: (LAResult) -> Unit completed: (LAResult) -> Unit
) { ) {
when { when (usingLAMode) {
SDK_INT in 28..29 -> LAMode.SYSTEM -> when {
// KeyguardManager.isDeviceSecure()? https://developer.android.com/training/sign-in/biometric-auth#declare-supported-authentication-types SDK_INT in 28..29 ->
authenticateWithBiometricManager(promptTitle, promptSubtitle, activity, completed, BIOMETRIC_WEAK or DEVICE_CREDENTIAL) // KeyguardManager.isDeviceSecure()? https://developer.android.com/training/sign-in/biometric-auth#declare-supported-authentication-types
SDK_INT > 29 -> authenticateWithBiometricManager(promptTitle, promptSubtitle, activity, completed, BIOMETRIC_WEAK or DEVICE_CREDENTIAL)
authenticateWithBiometricManager(promptTitle, promptSubtitle, activity, completed, BIOMETRIC_STRONG or DEVICE_CREDENTIAL) SDK_INT > 29 ->
else -> authenticateWithBiometricManager(promptTitle, promptSubtitle, activity, completed, BIOMETRIC_STRONG or DEVICE_CREDENTIAL)
completed(LAResult.Unavailable) else -> completed(LAResult.Unavailable())
}
LAMode.PASSCODE -> {
val password = ksAppPassword.get() ?: return completed(LAResult.Unavailable(generalGetString(R.string.la_no_app_password)))
ModalManager.shared.showCustomModal(animated = false) { close ->
BackHandler {
close()
completed(LAResult.Error(generalGetString(R.string.authentication_cancelled)))
}
Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background) {
LocalAuthView(SimplexApp.context.chatModel, LocalAuthRequest(promptTitle, promptSubtitle, password) {
close()
completed(it)
})
}
}
}
} }
} }
@@ -66,7 +101,7 @@ private fun authenticateWithBiometricManager(
override fun onAuthenticationFailed() { override fun onAuthenticationFailed() {
super.onAuthenticationFailed() super.onAuthenticationFailed()
completed(LAResult.Failed) completed(LAResult.Failed())
} }
} }
) )
@@ -78,9 +113,7 @@ private fun authenticateWithBiometricManager(
.build() .build()
biometricPrompt.authenticate(promptInfo) biometricPrompt.authenticate(promptInfo)
} }
else -> { else -> completed(LAResult.Unavailable())
completed(LAResult.Unavailable)
}
} }
} }
@@ -89,6 +122,18 @@ fun laTurnedOnAlert() = AlertManager.shared.showAlertMsg(
generalGetString(R.string.auth_you_will_be_required_to_authenticate_when_you_start_or_resume) generalGetString(R.string.auth_you_will_be_required_to_authenticate_when_you_start_or_resume)
) )
fun laPasscodeNotSetAlert() = AlertManager.shared.showAlertMsg(
generalGetString(R.string.lock_not_enabled),
generalGetString(R.string.you_can_turn_on_lock)
)
fun laFailedAlert() {
AlertManager.shared.showAlertMsg(
title = generalGetString(R.string.la_auth_failed),
text = generalGetString(R.string.la_could_not_be_verified)
)
}
fun laUnavailableInstructionAlert() = AlertManager.shared.showAlertMsg( fun laUnavailableInstructionAlert() = AlertManager.shared.showAlertMsg(
generalGetString(R.string.auth_unavailable), generalGetString(R.string.auth_unavailable),
generalGetString(R.string.auth_device_authentication_is_not_enabled_you_can_turn_on_in_settings_once_enabled) generalGetString(R.string.auth_device_authentication_is_not_enabled_you_can_turn_on_in_settings_once_enabled)
@@ -11,8 +11,8 @@ import androidx.compose.runtime.*
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import chat.simplex.app.TAG import chat.simplex.app.TAG
import chat.simplex.app.ui.theme.SettingsBackgroundLight
import chat.simplex.app.ui.theme.isInDarkTheme import chat.simplex.app.ui.theme.isInDarkTheme
import chat.simplex.app.ui.theme.themedBackground
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
@Composable @Composable
@@ -20,12 +20,13 @@ fun ModalView(
close: () -> Unit, close: () -> Unit,
background: Color = MaterialTheme.colors.background, background: Color = MaterialTheme.colors.background,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
endButtons: @Composable RowScope.() -> Unit = {},
content: @Composable () -> Unit, content: @Composable () -> Unit,
) { ) {
BackHandler(onBack = close) BackHandler(onBack = close)
Surface(Modifier.fillMaxSize()) { Surface(Modifier.fillMaxSize()) {
Column(Modifier.background(background)) { Column(if (background != MaterialTheme.colors.background) Modifier.background(background) else Modifier.themedBackground()) {
CloseSheetBar(close) CloseSheetBar(close, endButtons)
Box(modifier) { content() } Box(modifier) { content() }
} }
} }
@@ -37,15 +38,15 @@ class ModalManager {
private val toRemove = mutableSetOf<Int>() private val toRemove = mutableSetOf<Int>()
private var oldViewChanging = AtomicBoolean(false) private var oldViewChanging = AtomicBoolean(false)
fun showModal(settings: Boolean = false, content: @Composable () -> Unit) { fun showModal(settings: Boolean = false, endButtons: @Composable RowScope.() -> Unit = {}, content: @Composable () -> Unit) {
showCustomModal { close -> showCustomModal { close ->
ModalView(close, if (!settings || isInDarkTheme()) MaterialTheme.colors.background else SettingsBackgroundLight, content = content) ModalView(close, endButtons = endButtons, content = content)
} }
} }
fun showModalCloseable(settings: Boolean = false, content: @Composable (close: () -> Unit) -> Unit) { fun showModalCloseable(settings: Boolean = false, content: @Composable (close: () -> Unit) -> Unit) {
showCustomModal { close -> showCustomModal { close ->
ModalView(close, if (!settings || isInDarkTheme()) MaterialTheme.colors.background else SettingsBackgroundLight, content = { content(close) }) ModalView(close, content = { content(close) })
} }
} }
@@ -1,5 +1,6 @@
package chat.simplex.app.views.helpers package chat.simplex.app.views.helpers
import android.app.Application
import android.content.Context import android.content.Context
import android.media.* import android.media.*
import android.media.AudioManager.AudioPlaybackCallback import android.media.AudioManager.AudioPlaybackCallback
@@ -14,8 +15,6 @@ import chat.simplex.app.model.ChatItem
import chat.simplex.app.views.helpers.AudioPlayer.duration import chat.simplex.app.views.helpers.AudioPlayer.duration
import kotlinx.coroutines.* import kotlinx.coroutines.*
import java.io.* import java.io.*
import java.text.SimpleDateFormat
import java.util.*
interface Recorder { interface Recorder {
fun start(onProgressUpdate: (position: Int?, finished: Boolean) -> Unit): String fun start(onProgressUpdate: (position: Int?, finished: Boolean) -> Unit): String
@@ -26,6 +25,7 @@ class RecorderNative(private val recordedBytesLimit: Long): Recorder {
companion object { companion object {
// Allows to stop the recorder from outside without having the recorder in a variable // Allows to stop the recorder from outside without having the recorder in a variable
var stopRecording: (() -> Unit)? = null var stopRecording: (() -> Unit)? = null
const val extension = "m4a"
} }
private var recorder: MediaRecorder? = null private var recorder: MediaRecorder? = null
private var progressJob: Job? = null private var progressJob: Job? = null
@@ -39,6 +39,7 @@ class RecorderNative(private val recordedBytesLimit: Long): Recorder {
} }
override fun start(onProgressUpdate: (position: Int?, finished: Boolean) -> Unit): String { override fun start(onProgressUpdate: (position: Int?, finished: Boolean) -> Unit): String {
VideoPlayer.stopAll()
AudioPlayer.stop() AudioPlayer.stop()
val rec: MediaRecorder val rec: MediaRecorder
recorder = initRecorder().also { rec = it } recorder = initRecorder().also { rec = it }
@@ -50,8 +51,10 @@ class RecorderNative(private val recordedBytesLimit: Long): Recorder {
rec.setAudioEncodingBitRate(16000) rec.setAudioEncodingBitRate(16000)
rec.setMaxDuration(MAX_VOICE_MILLIS_FOR_SENDING) rec.setMaxDuration(MAX_VOICE_MILLIS_FOR_SENDING)
rec.setMaxFileSize(recordedBytesLimit) rec.setMaxFileSize(recordedBytesLimit)
val timestamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(Date()) val tmpDir = SimplexApp.context.getDir("temp", Application.MODE_PRIVATE)
val path = getAppFilePath(SimplexApp.context, uniqueCombine(SimplexApp.context, getAppFilePath(SimplexApp.context, "voice_${timestamp}.m4a"))) val fileToSave = File.createTempFile(generateNewFileName(SimplexApp.context, "voice", "${extension}_"), ".tmp", tmpDir)
fileToSave.deleteOnExit()
val path = fileToSave.absolutePath
filePath = path filePath = path
rec.setOutputFile(path) rec.setOutputFile(path)
rec.prepare() rec.prepare()
@@ -150,6 +153,7 @@ object AudioPlayer {
return null return null
} }
VideoPlayer.stopAll()
RecorderNative.stopRecording?.invoke() RecorderNative.stopRecording?.invoke()
val current = currentlyPlaying.value val current = currentlyPlaying.value
if (current == null || current.first != filePath) { if (current == null || current.first != filePath) {
@@ -8,8 +8,6 @@ import androidx.compose.foundation.text.*
import androidx.compose.material.* import androidx.compose.material.*
import androidx.compose.material.TextFieldDefaults.indicatorLine import androidx.compose.material.TextFieldDefaults.indicatorLine
import androidx.compose.material.TextFieldDefaults.textFieldWithLabelPadding import androidx.compose.material.TextFieldDefaults.textFieldWithLabelPadding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.ExperimentalComposeUiApi
@@ -17,7 +15,9 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.* import androidx.compose.ui.graphics.*
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
@@ -29,15 +29,18 @@ import kotlinx.coroutines.delay
@OptIn(ExperimentalComposeUiApi::class) @OptIn(ExperimentalComposeUiApi::class)
@Composable @Composable
fun SearchTextField(modifier: Modifier, placeholder: String, onValueChange: (String) -> Unit) { fun SearchTextField(modifier: Modifier, placeholder: String, alwaysVisible: Boolean, onValueChange: (String) -> Unit) {
var searchText by rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue("")) } var searchText by rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue("")) }
val focusRequester = remember { FocusRequester() } val focusRequester = remember { FocusRequester() }
val focusManager = LocalFocusManager.current
val keyboard = LocalSoftwareKeyboardController.current val keyboard = LocalSoftwareKeyboardController.current
LaunchedEffect(Unit) { if (!alwaysVisible) {
focusRequester.requestFocus() LaunchedEffect(Unit) {
delay(200) focusRequester.requestFocus()
keyboard?.show() delay(200)
keyboard?.show()
}
} }
DisposableEffect(Unit) { DisposableEffect(Unit) {
@@ -87,8 +90,15 @@ fun SearchTextField(modifier: Modifier, placeholder: String, onValueChange: (Str
Text(placeholder) Text(placeholder)
}, },
trailingIcon = if (searchText.text.isNotEmpty()) {{ trailingIcon = if (searchText.text.isNotEmpty()) {{
IconButton({ searchText = TextFieldValue(""); onValueChange("") }) { IconButton({
Icon(Icons.Default.Close, stringResource(R.string.icon_descr_close_button), tint = MaterialTheme.colors.primary,) if (alwaysVisible) {
keyboard?.hide()
focusManager.clearFocus()
}
searchText = TextFieldValue("");
onValueChange("")
}) {
Icon(painterResource(R.drawable.ic_close), stringResource(R.string.icon_descr_close_button), tint = MaterialTheme.colors.primary,)
} }
}} else null, }} else null,
singleLine = true, singleLine = true,
@@ -1,42 +1,43 @@
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.* import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Check
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.* import androidx.compose.ui.unit.*
import chat.simplex.app.R
import chat.simplex.app.ui.theme.* import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.helpers.ValueTitleDesc import chat.simplex.app.views.helpers.ValueTitleDesc
import chat.simplex.app.views.helpers.ValueTitle import chat.simplex.app.views.helpers.ValueTitle
import chat.simplex.app.views.usersettings.SettingsActionItemWithContent
@Composable @Composable
fun SectionView(title: String? = null, padding: PaddingValues = PaddingValues(), content: (@Composable ColumnScope.() -> Unit)) { fun SectionView(title: String? = null, padding: PaddingValues = PaddingValues(), content: (@Composable ColumnScope.() -> Unit)) {
Column { Column {
if (title != null) { if (title != null) {
Text( Text(
title, color = HighOrLowlight, style = MaterialTheme.typography.body2, title, color = MaterialTheme.colors.secondary, style = MaterialTheme.typography.body2,
modifier = Modifier.padding(start = DEFAULT_PADDING, bottom = 5.dp), fontSize = 12.sp modifier = Modifier.padding(start = DEFAULT_PADDING, bottom = 5.dp), fontSize = 12.sp
) )
} }
Surface(color = if (isInDarkTheme()) GroupDark else MaterialTheme.colors.background) { Column(Modifier.padding(padding).fillMaxWidth()) { content() }
Column(Modifier.padding(padding).fillMaxWidth()) { content() }
}
} }
} }
@Composable @Composable
fun SectionView( fun SectionView(
title: String, title: String,
icon: ImageVector, icon: Painter,
iconTint: Color = HighOrLowlight, iconTint: Color = MaterialTheme.colors.secondary,
leadingIcon: Boolean = false, leadingIcon: Boolean = false,
padding: PaddingValues = PaddingValues(), padding: PaddingValues = PaddingValues(),
content: (@Composable ColumnScope.() -> Unit) content: (@Composable ColumnScope.() -> Unit)
@@ -44,13 +45,11 @@ fun SectionView(
Column { Column {
val iconSize = with(LocalDensity.current) { 21.sp.toDp() } val iconSize = with(LocalDensity.current) { 21.sp.toDp() }
Row(Modifier.padding(start = DEFAULT_PADDING, bottom = 5.dp), verticalAlignment = Alignment.CenterVertically) { Row(Modifier.padding(start = DEFAULT_PADDING, bottom = 5.dp), verticalAlignment = Alignment.CenterVertically) {
if (leadingIcon) Icon(icon, null, Modifier.padding(end = 4.dp).size(iconSize), tint = iconTint) if (leadingIcon) Icon(icon, null, Modifier.padding(end = DEFAULT_PADDING_HALF).size(iconSize), tint = iconTint)
Text(title, color = HighOrLowlight, style = MaterialTheme.typography.body2, fontSize = 12.sp) Text(title, color = MaterialTheme.colors.secondary, style = MaterialTheme.typography.body2, fontSize = 12.sp)
if (!leadingIcon) Icon(icon, null, Modifier.padding(start = 4.dp).size(iconSize), tint = iconTint) if (!leadingIcon) Icon(icon, null, Modifier.padding(start = DEFAULT_PADDING_HALF).size(iconSize), tint = iconTint)
}
Surface(color = if (isInDarkTheme()) GroupDark else MaterialTheme.colors.background) {
Column(Modifier.padding(padding).fillMaxWidth()) { content() }
} }
Column(Modifier.padding(padding).fillMaxWidth()) { content() }
} }
} }
@@ -68,7 +67,7 @@ fun <T> SectionViewSelectable(
SectionItemViewSpaceBetween({ onSelected(item.value) }) { SectionItemViewSpaceBetween({ onSelected(item.value) }) {
Text(item.title) Text(item.title)
if (currentValue.value == item.value) { if (currentValue.value == item.value) {
Icon(Icons.Outlined.Check, item.title, tint = MaterialTheme.colors.primary) Icon(painterResource(R.drawable.ic_check), item.title, tint = MaterialTheme.colors.primary)
} }
} }
Spacer(Modifier.padding(horizontal = 4.dp)) Spacer(Modifier.padding(horizontal = 4.dp))
@@ -83,13 +82,37 @@ fun SectionItemView(
click: (() -> Unit)? = null, click: (() -> Unit)? = null,
minHeight: Dp = 46.dp, minHeight: Dp = 46.dp,
disabled: Boolean = false, disabled: Boolean = false,
extraPadding: Boolean = false,
padding: PaddingValues = if (extraPadding)
PaddingValues(start = DEFAULT_PADDING * 1.7f, end = DEFAULT_PADDING)
else
PaddingValues(horizontal = DEFAULT_PADDING),
content: (@Composable RowScope.() -> Unit) content: (@Composable RowScope.() -> Unit)
) { ) {
val modifier = Modifier val modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.sizeIn(minHeight = minHeight) .sizeIn(minHeight = minHeight)
Row( Row(
if (click == null || disabled) modifier.padding(horizontal = DEFAULT_PADDING) else modifier.clickable(onClick = click).padding(horizontal = DEFAULT_PADDING), if (click == null || disabled) modifier.padding(padding) else modifier.clickable(onClick = click).padding(padding),
verticalAlignment = Alignment.CenterVertically
) {
content()
}
}
@Composable
fun SectionItemViewWithIcon(
click: (() -> Unit)? = null,
minHeight: Dp = 46.dp,
disabled: Boolean = false,
padding: PaddingValues = PaddingValues(start = DEFAULT_PADDING * 1.7f, end = DEFAULT_PADDING),
content: (@Composable RowScope.() -> Unit)
) {
val modifier = Modifier
.fillMaxWidth()
.sizeIn(minHeight = minHeight)
Row(
if (click == null || disabled) modifier.padding(padding) else modifier.clickable(onClick = click).padding(padding),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
content() content()
@@ -99,6 +122,7 @@ fun SectionItemView(
@Composable @Composable
fun SectionItemViewSpaceBetween( fun SectionItemViewSpaceBetween(
click: (() -> Unit)? = null, click: (() -> Unit)? = null,
onLongClick: (() -> Unit)? = null,
minHeight: Dp = 46.dp, minHeight: Dp = 46.dp,
padding: PaddingValues = PaddingValues(horizontal = DEFAULT_PADDING), padding: PaddingValues = PaddingValues(horizontal = DEFAULT_PADDING),
disabled: Boolean = false, disabled: Boolean = false,
@@ -108,7 +132,7 @@ fun SectionItemViewSpaceBetween(
.fillMaxWidth() .fillMaxWidth()
.sizeIn(minHeight = minHeight) .sizeIn(minHeight = minHeight)
Row( Row(
if (click == null || disabled) modifier.padding(padding) else modifier.clickable(onClick = click).padding(padding), if (click == null || disabled) modifier.padding(padding) else modifier.combinedClickable(onClick = click, onLongClick = onLongClick).padding(padding),
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
@@ -122,24 +146,12 @@ fun <T> SectionItemWithValue(
currentValue: State<T>, currentValue: State<T>,
values: List<ValueTitle<T>>, values: List<ValueTitle<T>>,
label: String? = null, label: String? = null,
icon: ImageVector? = null, icon: Painter? = null,
iconTint: Color = HighOrLowlight, iconTint: Color = MaterialTheme.colors.secondary,
enabled: State<Boolean> = mutableStateOf(true), enabled: State<Boolean> = mutableStateOf(true),
onSelected: () -> Unit onSelected: () -> Unit
) { ) {
SectionItemView(click = if (enabled.value) onSelected else null) { SettingsActionItemWithContent(icon = icon, text = title, iconColor = iconTint, click = if (enabled.value) onSelected else null, disabled = !enabled.value) {
if (icon != null) {
Icon(
icon,
title,
Modifier.padding(end = 8.dp),
tint = iconTint
)
}
Text(title, color = if (enabled.value) Color.Unspecified else HighOrLowlight)
Spacer(Modifier.fillMaxWidth().weight(1f))
Row( Row(
Modifier.padding(start = 10.dp), Modifier.padding(start = 10.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
@@ -149,7 +161,7 @@ fun <T> SectionItemWithValue(
values.first { it.value == currentValue.value }.title + (if (label != null) " $label" else ""), values.first { it.value == currentValue.value }.title + (if (label != null) " $label" else ""),
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
color = HighOrLowlight color = MaterialTheme.colors.secondary
) )
} }
} }
@@ -157,10 +169,15 @@ fun <T> SectionItemWithValue(
@Composable @Composable
fun SectionTextFooter(text: String) { fun SectionTextFooter(text: String) {
SectionTextFooter(AnnotatedString(text))
}
@Composable
fun SectionTextFooter(text: AnnotatedString) {
Text( Text(
text, text,
Modifier.padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, top = DEFAULT_PADDING_HALF).fillMaxWidth(0.9F), Modifier.padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, top = DEFAULT_PADDING_HALF).fillMaxWidth(0.9F),
color = HighOrLowlight, color = MaterialTheme.colors.secondary,
lineHeight = 18.sp, lineHeight = 18.sp,
fontSize = 14.sp fontSize = 14.sp
) )
@@ -180,20 +197,41 @@ fun SectionDivider() {
Divider(Modifier.padding(horizontal = 8.dp)) Divider(Modifier.padding(horizontal = 8.dp))
} }
@Composable
fun SectionDividerSpaced(maxTopPadding: Boolean = false, maxBottomPadding: Boolean = true) {
Divider(
Modifier.padding(
start = DEFAULT_PADDING_HALF,
top = if (maxTopPadding) 40.dp else 30.dp,
end = DEFAULT_PADDING_HALF,
bottom = if (maxBottomPadding) 40.dp else 30.dp)
)
}
@Composable @Composable
fun SectionSpacer() { fun SectionSpacer() {
Spacer(Modifier.height(30.dp)) Spacer(Modifier.height(30.dp))
} }
@Composable @Composable
fun InfoRow(title: String, value: String, icon: ImageVector? = null, iconTint: Color? = null) { fun SectionBottomSpacer() {
Spacer(Modifier.height(DEFAULT_BOTTOM_PADDING))
}
@Composable
fun TextIconSpaced(extraPadding: Boolean = false) {
Spacer(Modifier.padding(horizontal = if (extraPadding) 17.dp else DEFAULT_PADDING_HALF))
}
@Composable
fun InfoRow(title: String, value: String, icon: Painter? = null, iconTint: Color? = null) {
SectionItemViewSpaceBetween { SectionItemViewSpaceBetween {
Row { Row {
val iconSize = with(LocalDensity.current) { 21.sp.toDp() } val iconSize = with(LocalDensity.current) { 21.sp.toDp() }
if (icon != null) Icon(icon, title, Modifier.padding(end = 8.dp).size(iconSize), tint = iconTint ?: HighOrLowlight) if (icon != null) Icon(icon, title, Modifier.padding(end = 8.dp).size(iconSize), tint = iconTint ?: MaterialTheme.colors.secondary)
Text(title) Text(title)
} }
Text(value, color = HighOrLowlight) Text(value, color = MaterialTheme.colors.secondary)
} }
} }
@@ -209,7 +247,7 @@ fun InfoRowEllipsis(title: String, value: String, onClick: () -> Unit) {
.widthIn(max = (configuration.screenWidthDp / 2).dp), .widthIn(max = (configuration.screenWidthDp / 2).dp),
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
color = HighOrLowlight color = MaterialTheme.colors.secondary
) )
} }
} }

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