Compare commits

..

44 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
1042 changed files with 27092 additions and 115194 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
name: Bug name: Bug
description: File a bug report/issue description: File a bug report/issue
title: "[Bug]: " title: "[Bug]: "
labels: ["bug", "triage"] labels: ["type:bug", "type:triage"]
body: body:
- type: checkboxes - type: checkboxes
attributes: attributes:
+1 -1
View File
@@ -1,7 +1,7 @@
name: Feature name: Feature
description: Suggest your feature description: Suggest your feature
title: "[Feature]: " title: "[Feature]: "
labels: ["enhancement", "triage"] labels: ["type:enhancement", "type:triage"]
body: body:
- type: checkboxes - type: checkboxes
attributes: attributes:
+1 -1
View File
@@ -1,7 +1,7 @@
name: Question name: Question
description: Ask your question description: Ask your question
title: "[Q]: " title: "[Q]: "
labels: ["question", "triage"] labels: ["type:question", "type:triage"]
body: body:
- type: markdown - type: markdown
attributes: attributes:
+11 -19
View File
@@ -16,11 +16,11 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Clone project - name: Clone project
uses: actions/checkout@v3 uses: actions/checkout@v2
- name: Build changelog - name: Build changelog
id: build_changelog id: build_changelog
uses: mikepenz/release-changelog-builder-action@v4 uses: mikepenz/release-changelog-builder-action@v1
with: with:
configuration: .github/changelog_conf.json configuration: .github/changelog_conf.json
failOnError: true failOnError: true
@@ -62,25 +62,17 @@ jobs:
cache_path: C:/cabal cache_path: C:/cabal
asset_name: simplex-chat-windows-x86-64 asset_name: simplex-chat-windows-x86-64
steps: steps:
- name: Configure pagefile (Windows)
if: matrix.os == 'windows-latest'
uses: al-cheb/configure-pagefile-action@v1.3
with:
minimum-size: 16GB
maximum-size: 16GB
disk-root: "C:"
- name: Clone project - name: Clone project
uses: actions/checkout@v3 uses: actions/checkout@v2
- name: Setup Haskell - name: Setup Haskell
uses: haskell/actions/setup@v2 uses: haskell/actions/setup@v1
with: with:
ghc-version: "8.10.7" ghc-version: "8.10.7"
cabal-version: "latest" cabal-version: "latest"
- name: Cache dependencies - name: Cache dependencies
uses: actions/cache@v3 uses: actions/cache@v2
with: with:
path: | path: |
${{ matrix.cache_path }} ${{ matrix.cache_path }}
@@ -119,6 +111,12 @@ jobs:
cabal build --enable-tests cabal build --enable-tests
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
if: matrix.os != 'windows-latest'
timeout-minutes: 30
shell: bash
run: cabal test --test-show-details=direct
- name: Unix upload binary to release - name: Unix upload binary to release
if: startsWith(github.ref, 'refs/tags/v') && matrix.os != 'windows-latest' if: startsWith(github.ref, 'refs/tags/v') && matrix.os != 'windows-latest'
uses: svenstaro/upload-release-action@v2 uses: svenstaro/upload-release-action@v2
@@ -128,12 +126,6 @@ jobs:
asset_name: ${{ matrix.asset_name }} asset_name: ${{ matrix.asset_name }}
tag: ${{ github.ref }} tag: ${{ github.ref }}
- name: Unix test
if: matrix.os != 'windows-latest'
timeout-minutes: 30
shell: bash
run: cabal test --test-show-details=direct
# Unix / # Unix /
# / Windows # / Windows
+1 -1
View File
@@ -17,7 +17,7 @@ jobs:
strategy: strategy:
matrix: matrix:
node-version: [16.x] node-version: [12.x]
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
+1 -1
View File
@@ -49,8 +49,8 @@ logs/
# for website # for website
website/node_modules/ website/node_modules/
website/src/blog/ website/src/blog/
website/src/docs/
website/translations.json 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
+15 -35
View File
@@ -64,18 +64,11 @@ You must:
Messages not following these rules will be deleted, the right to send messages may be revoked, and the access to the new members to the group may be temporarily restricted, to prevent re-joining under a different name - our imperfect group moderation does not have a better solution at the moment. Messages not following these rules will be deleted, the right to send messages may be revoked, and the access to the new members to the group may be temporarily restricted, to prevent re-joining under a different name - our imperfect group moderation does not have a better solution at the moment.
You can join an English-speaking users group if you want to ask any questions: [#SimpleX-Group-4](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2Fw2GlucRXtRVgYnbt_9ZP-kmt76DekxxS%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEA0tJhTyMGUxznwmjb7aT24P1I1Wry_iURTuhOFlMb1Eo%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22WoPxjFqGEDlVazECOSi2dg%3D%3D%22%7D) You can join an English-speaking users group if you want to ask any questions: [#SimpleX-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 is also a group [#simplex-devs](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2F6eHqy7uAbZPOcA6qBtrQgQquVlt4Ll91%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAqV_pg3FF00L98aCXp4D3bOs4Sxv_UmSd-gb0juVoQVs%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22XonlixcHBIb2ijCehbZoiw%3D%3D%22%7D) for developers who build on SimpleX platform:
- chat bots and automations
- integrations with other apps
- social apps and services
- etc.
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: There are groups in other languages, that we have the apps interface translated into. These groups are for testing, and asking questions to other SimpleX Chat users:
[\#SimpleX-DE](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FkIEl7OQzcp-J6aDmjdlQbRJwqkcZE7XR%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAR16PCu02MobRmKAsjzhDWMZcWP9hS8l5AUZi-Gs8z18%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22puYPMCQt11yPUvgmI5jCiw%3D%3D%22%7D) (German-speaking), [\#SimpleX-ES](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FaJ8O1O8A8GbeoaHTo_V8dcefaCl7ouPb%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEA034qWTA3sWcTsi6aWhNf9BA34vKVCFaEBdP2R66z6Ao%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22wiZ1v_wNjLPlT-nCSB-bRA%3D%3D%22%7D) (Spanish-speaking), [\#SimpleX-FR](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2Fhpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg%3D%40smp5.simplex.im%2FvIHQDxTor53nwnWWTy5cHNwQQAdWN5Hw%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAPdgK1eBnETmgiqEQufbUkydKBJafoRx4iRrtrC2NAGc%253D%26srv%3Djjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%221FyUryBPza-1ZFFE80Ekbg%3D%3D%22%7D) (French-speaking), [\#SimpleX-RU](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FXZyt3hJmWsycpN7Dqve_wbrAqb6myk1R%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAMFVIoytozTEa_QXOgoZFq_oe0IwZBYKvW50trSFXzXo%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22xz05ngjA3pNIxLZ32a8Vxg%3D%3D%22%7D) (Russian-speaking), [\#SimpleX-IT](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2F0weR-ZgDUl7ruOtI_8TZwEsnJP6UiImA%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAq4PSThO9Fvb5ydF48wB0yNbpzCbuQJCW3vZ9BGUfcxk%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22e-iceLA0SctC62eARgYDWg%3D%3D%22%7D) (Italian-speaking). [\#SimpleX-DE](https://simplex.chat/contact#/?v=1-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. You can join either by opening these links in the app or by opening them in a desktop browser and scanning the QR code.
@@ -108,10 +101,8 @@ Join our translators to help SimpleX grow!
|🇪🇸 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/)|| |🇪🇸 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)| |🇫🇷 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/)|| |🇮🇹 it|Italiano |[unbranched](https://github.com/unbranched)|[![android app](https://hosted.weblate.org/widgets/simplex-chat/it/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/it/)<br>[![ios app](https://hosted.weblate.org/widgets/simplex-chat/it/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/it/)|[![website](https://hosted.weblate.org/widgets/simplex-chat/it/website/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/website/it/)||
|🇯🇵 ja|Japanese ||[![android app](https://hosted.weblate.org/widgets/simplex-chat/it/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/ja/)<br>[![ios app](https://hosted.weblate.org/widgets/simplex-chat/ja/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/ja/)|||
|🇳🇱 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/)|| |🇳🇱 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/)||| |🇵🇱 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/)|||
|🇧🇷 pt-BR|Português||[![android app](https://hosted.weblate.org/widgets/simplex-chat/pt_BR/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/pt_BR/)<br>-|[![website](https://hosted.weblate.org/widgets/simplex-chat/pt_BR/website/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/website/pt_BR/)||
|🇷🇺 ru|Русский ||[![android app](https://hosted.weblate.org/widgets/simplex-chat/ru/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/ru/)<br>[![ios app](https://hosted.weblate.org/widgets/simplex-chat/ru/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/ru/)||| |🇷🇺 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/)|| |🇨🇳 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/)||
@@ -121,7 +112,6 @@ Languages in progress: Arabic, Japanese, Korean, Portuguese and [others](https:/
We would love to have you join the development! You can help us with: We would love to have you join the development! You can help us with:
- [share the color theme](./docs/THEMES.md) you use in Android app!
- writing a tutorial or recipes about hosting servers, chat bot automations, etc. - writing a tutorial or recipes about hosting servers, chat bot automations, etc.
- contributing to SimpleX Chat knowledge-base. - contributing to SimpleX Chat knowledge-base.
- developing features - please connect to us via chat so we can help you get started. - developing features - please connect to us via chat so we can help you get started.
@@ -207,10 +197,6 @@ You can use SimpleX with your own servers and still communicate with people usin
Recent updates: Recent updates:
[July 22, 2023. SimpleX Chat: v5.2 released with message delivery receipts](./blog/20230722-simplex-chat-v5-2-message-delivery-receipts.md).
[May 23, 2023. SimpleX Chat: v5.1 released with message reactions and self-destruct passcode](./blog/20230523-simplex-chat-v5-1-message-reactions-self-destruct-passcode.md).
[Apr 22, 2023. SimpleX Chat: vision and funding, v5.0 released with videos and files up to 1gb](./blog/20230422-simplex-chat-vision-funding-v5-videos-files-passcode.md). [Apr 22, 2023. SimpleX Chat: vision and funding, v5.0 released with videos and files up to 1gb](./blog/20230422-simplex-chat-vision-funding-v5-videos-files-passcode.md).
[Mar 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). [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).
@@ -259,15 +245,13 @@ See [SimpleX Chat Protocol](./docs/protocol/simplex-chat.md) for the format of m
SimpleX Chat is a work in progress we are releasing improvements as they are ready. You have to decide if the current state is good enough for your usage scenario. SimpleX Chat is a work in progress we are releasing improvements as they are ready. You have to decide if the current state is good enough for your usage scenario.
We compiled a [glossary of terms](./docs/GLOSSARY.md) used to describe communication systems to help understand some terms below and to help compare advantages and disadvantages of various communication systems.
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](./docs/GLOSSARY.md#pairwise-pseudonymous-identifier) (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. 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](./docs/GLOSSARY.md#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](./docs/GLOSSARY.md#double-ratchet-algorithm) 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](./docs/GLOSSARY.md#forward-secrecy) (each message is encrypted by its own ephemeral key) and [break-in recovery](./docs/GLOSSARY.md#post-compromise-security) (the keys are frequently re-negotiated as part of the message exchange). Two pairs of Curve448 keys are used for the initial [key agreement](./docs/GLOSSARY.md#key-agreement-protocol), 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).
5. Several levels of [content padding](./docs/GLOSSARY.md#message-padding) to frustrate message size attacks. 5. Several levels of content padding to frustrate message size attacks.
6. Starting from v2 of SMP protocol (the current version is v4) all message metadata, including the time when the message was received by the server (rounded to a second) is sent to the recipients inside an encrypted envelope, so even if TLS is compromised it cannot be observed. 6. Starting from v2 of SMP protocol (the current version is v4) all message metadata, including the time when the message was received by the server (rounded to a second) is sent to the recipients inside an encrypted envelope, so even if TLS is compromised it cannot be observed.
7. Only TLS 1.2/1.3 are allowed for client-server connections, limited to cryptographic algorithms: CHACHA20POLY1305_SHA256, Ed25519/Ed448, Curve25519/Curve448. 7. Only TLS 1.2/1.3 are allowed for client-server connections, limited to cryptographic algorithms: CHACHA20POLY1305_SHA256, Ed25519/Ed448, Curve25519/Curve448.
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.
@@ -292,8 +276,6 @@ You can:
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.
Please also join [#simplex-devs](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2F6eHqy7uAbZPOcA6qBtrQgQquVlt4Ll91%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAqV_pg3FF00L98aCXp4D3bOs4Sxv_UmSd-gb0juVoQVs%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22XonlixcHBIb2ijCehbZoiw%3D%3D%22%7D) group to ask any questions and share your success stories.
## Roadmap ## Roadmap
- ✅ Easy to deploy SimpleX server with in-memory message storage, without any dependencies. - ✅ Easy to deploy SimpleX server with in-memory message storage, without any dependencies.
@@ -334,27 +316,25 @@ Please also join [#simplex-devs](https://simplex.chat/contact#/?v=1-2&smp=smp%3A
- ✅ Sending and receiving large files via [XFTP protocol](./blog/20230301-simplex-file-transfer-protocol.md). - ✅ Sending and receiving large files via [XFTP protocol](./blog/20230301-simplex-file-transfer-protocol.md).
- ✅ Video messages. - ✅ Video messages.
- ✅ App access passcode. - ✅ App access passcode.
- Improved Android app UI design. - 🏗 Improved Android app UI design.
- ✅ Optional alternative access password. - 🏗 SMP queue redundancy and rotation (manual is supported).
- ✅ Message reactions - 🏗 Reduced battery and traffic usage in large groups.
- ✅ Message editing history
- ✅ Reduced battery and traffic usage in large groups.
- ✅ Message delivery confirmation (with sender opt-out per contact).
- 🏗 Desktop client.
- SMP queue redundancy and rotation (manual is supported).
- Include optional message into connection request sent via contact address. - Include optional message into connection request sent via contact address.
- Ephemeral/disappearing/OTR conversations with the existing contacts.
- Optional alternative access password.
- Local app files encryption. - Local app files encryption.
- Improved navigation and search in the conversation (expand and scroll to quoted message, scroll to search results, etc.). - Improved navigation and search in the conversation (expand and scroll to quoted message, scroll to search results, etc.).
- Large groups, communities and public channels. - Message delivery confirmation (with sender opt-in or opt-out per contact, TBC).
- Feeds/broadcasts.
- Ephemeral/disappearing/OTR conversations with the existing contacts.
- Privately share your location. - Privately share your location.
- 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.).
- Supporting the same profile on multiple devices. - Supporting the same profile on multiple devices.
- Desktop client.
- 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.
- 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). - Message delivery relay for senders (to conceal IP address from the recipients' servers and to reduce the traffic).
- High capacity multi-node SMP relays. - High capacity multi-node SMP relays.
+20
View File
@@ -0,0 +1,20 @@
*.iml
.gradle
/local.properties
/.idea/caches
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
/.idea/deploymentTargetDropDown.xml
/.idea/misc.xml
/.idea/uiDesigner.xml
/.idea/kotlinc.xml
.DS_Store
/build
/captures
.externalNativeBuild
.cxx
local.properties
app/src/main/cpp/libs/
+1
View File
@@ -0,0 +1 @@
SimpleX
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="11" />
</component>
</project>
+19
View File
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleMigrationSettings" migrationVersion="1" />
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="testRunner" value="GRADLE" />
<option name="distributionType" value="DEFAULT_WRAPPED" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
</set>
</option>
</GradleProjectSettings>
</option>
</component>
</project>
@@ -0,0 +1,20 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="PreviewAnnotationInFunctionWithParameters" enabled="true" level="ERROR" enabled_by_default="true">
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewMultipleParameterProviders" enabled="true" level="ERROR" enabled_by_default="true">
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
<option name="previewFile" value="true" />
</inspection_tool>
</profile>
</component>
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$/../.." vcs="Git" />
</component>
</project>
@@ -4,6 +4,7 @@ This readme is currently a stub and as such is in development.
Ultimately, this readme will act as a guide to contributing to the develop of the SimpleX android app. Ultimately, this readme will act as a guide to contributing to the develop of the SimpleX android app.
## Gotchas ## Gotchas
#### SHA Signature for verification for app links/deep links #### SHA Signature for verification for app links/deep links
@@ -22,15 +23,3 @@ To find your SHA certificate fingerprint perform the following steps.
More information is available [here](https://developer.android.com/training/app-links/verify-site-associations#manual-verification). If there is no response when running the `pm get-app-links` command, the intents in `AndroidManifest.xml` are likely misspecified. A verification attempt can be triggered using `adb shell pm verify-app-links --re-verify chat.simplex.app`. More information is available [here](https://developer.android.com/training/app-links/verify-site-associations#manual-verification). If there is no response when running the `pm get-app-links` command, the intents in `AndroidManifest.xml` are likely misspecified. A verification attempt can be triggered using `adb shell pm verify-app-links --re-verify chat.simplex.app`.
Note that this is not an issue for the app store build of the app as this is signed with our app store credentials and thus there is a stable signature over users. Developers do not have general access to these credentials for development and testing. Note that this is not an issue for the app store build of the app as this is signed with our app store credentials and thus there is a stable signature over users. Developers do not have general access to these credentials for development and testing.
## Adding icons
1. Find a [Material symbol](https://fonts.google.com/icons?icon.style=Rounded) in Rounded category.
2. Set weight to 400, grade to -25 and size to 48px.
3. Click on the icon, choose Android and download XML file.
4. Update the color to black (#FF000000) and the size to "24.dp", as in other icons.
For example, this is [add reaction icon](https://fonts.google.com/icons?selected=Material+Symbols+Rounded:add_reaction:FILL@0;wght@300;GRAD@-25;opsz@24&icon.style=Rounded).
+235
View File
@@ -0,0 +1,235 @@
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
id 'org.jetbrains.kotlin.plugin.serialization'
}
android {
compileSdk 32
defaultConfig {
applicationId "chat.simplex.app"
minSdk 26
targetSdk 32
versionCode 117
versionName "5.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
useSupportLibrary true
}
externalNativeBuild {
cmake {
cppFlags ''
}
}
manifestPlaceholders.app_name = "@string/app_name"
manifestPlaceholders.provider_authorities = "chat.simplex.app.provider"
manifestPlaceholders.extract_native_libs = compression_level != "0"
}
buildTypes {
debug {
applicationIdSuffix "$application_id_suffix"
debuggable new Boolean("$enable_debuggable")
manifestPlaceholders.app_name = "$app_name"
// Provider can't be the same for different apps on the same device
manifestPlaceholders.provider_authorities = "chat.simplex.app${application_id_suffix}.provider"
}
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
freeCompilerArgs += "-opt-in=kotlinx.coroutines.DelicateCoroutinesApi"
freeCompilerArgs += "-opt-in=androidx.compose.foundation.ExperimentalFoundationApi"
freeCompilerArgs += "-opt-in=androidx.compose.ui.text.ExperimentalTextApi"
freeCompilerArgs += "-opt-in=androidx.compose.material.ExperimentalMaterialApi"
freeCompilerArgs += "-opt-in=com.google.accompanist.insets.ExperimentalAnimatedInsets"
freeCompilerArgs += "-opt-in=com.google.accompanist.permissions.ExperimentalPermissionsApi"
freeCompilerArgs += "-opt-in=kotlinx.serialization.InternalSerializationApi"
freeCompilerArgs += "-opt-in=kotlinx.serialization.ExperimentalSerializationApi"
}
externalNativeBuild {
cmake {
path file('src/main/cpp/CMakeLists.txt')
}
}
buildFeatures {
compose true
}
composeOptions {
kotlinCompilerExtensionVersion compose_version
}
packagingOptions {
resources {
excludes += '/META-INF/{AL2.0,LGPL2.1}'
}
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 {
implementation 'androidx.core:core-ktx:1.7.0'
implementation "androidx.compose.ui:ui:$compose_version"
implementation "androidx.compose.material:material:$compose_version"
implementation "androidx.compose.ui:ui-tooling-preview:$compose_version"
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.4.1'
implementation 'androidx.lifecycle:lifecycle-process:2.4.1'
implementation 'androidx.activity:activity-compose:1.4.0'
implementation 'androidx.fragment:fragment:1.4.1'
implementation 'org.jetbrains.kotlinx:kotlinx-datetime:0.3.2'
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.2'
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.navigation:navigation-compose:2.4.1"
implementation "com.google.accompanist:accompanist-insets:0.23.0"
implementation 'androidx.webkit:webkit:1.4.0'
implementation "com.godaddy.android.colorpicker:compose-color-picker:0.4.2"
def work_version = "2.7.1"
implementation "androidx.work:work-runtime-ktx:$work_version"
implementation "androidx.work:work-multiprocess:$work_version"
def camerax_version = "1.1.0-beta01"
implementation "androidx.camera:camera-core:${camerax_version}"
implementation "androidx.camera:camera-camera2:${camerax_version}"
implementation "androidx.camera:camera-lifecycle:${camerax_version}"
implementation "androidx.camera:camera-view:${camerax_version}"
//Barcode
implementation 'org.boofcv:boofcv-android:0.40.1'
implementation 'org.boofcv:boofcv-core:0.40.1'
//Camera Permission
implementation "com.google.accompanist:accompanist-permissions:0.23.0"
implementation "com.google.accompanist:accompanist-pager:0.25.1"
// Link Previews
implementation 'org.jsoup:jsoup:1.13.1'
// Biometric authentication
implementation 'androidx.biometric:biometric:1.2.0-alpha04'
// GIFs support
implementation "io.coil-kt:coil-compose: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'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version"
debugImplementation "androidx.compose.ui:ui-tooling:$compose_version"
}
// Don't do anything if no compression is needed
if (compression_level != "0") {
tasks.whenTaskAdded { task ->
if (task.name == 'packageDebug') {
task.finalizedBy compressApk
} else if (task.name == 'packageRelease') {
task.finalizedBy compressApk
}
}
}
tasks.register("compressApk") {
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 sdkDir = android.getSdkDirectory().getAbsolutePath()
def keyAlias = ""
def keyPassword = ""
def storeFile = ""
def storePassword = ""
if (project.properties['android.injected.signing.key.alias'] != null) {
keyAlias = project.properties['android.injected.signing.key.alias']
keyPassword = project.properties['android.injected.signing.key.password']
storeFile = project.properties['android.injected.signing.store.file']
storePassword = project.properties['android.injected.signing.store.password']
} else if (android.signingConfigs.hasProperty(buildType)) {
def gradleConfig = android.signingConfigs[buildType]
keyAlias = gradleConfig.keyAlias
keyPassword = gradleConfig.keyPassword
storeFile = gradleConfig.storeFile
storePassword = gradleConfig.storePassword
} else {
// There is no signing config for current build type, can't sign the apk
println("No signing configs for this build type: $buildType")
return
}
def outputDir = tasks["package${buildType.capitalize()}"].outputs.files.last()
exec {
workingDir '../../../scripts/android'
setEnvironment(['JAVA_HOME': "$javaHome"])
commandLine './compress-and-sign-apk.sh', \
"$compression_level", \
"$outputDir", \
"$sdkDir", \
"$storeFile", \
"$storePassword", \
"$keyAlias", \
"$keyPassword"
}
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-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
// project.properties.each { k, v -> println "$k -> $v" }
}
}
@@ -6,7 +6,6 @@
<uses-feature android:name="android.hardware.camera" /> <uses-feature android:name="android.hardware.camera" />
<uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.VIDEO_CAPTURE" /> <uses-permission android:name="android.permission.VIDEO_CAPTURE" />
<uses-permission android:name="android.permission.RECORD_AUDIO" /> <uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" /> <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 32 KiB

@@ -10,19 +10,23 @@ import android.view.WindowManager
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.compose.animation.core.* import androidx.compose.animation.core.*
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.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.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.MainActivity.Companion.enteredBackground import chat.simplex.app.MainActivity.Companion.enteredBackground
import chat.simplex.app.model.* import chat.simplex.app.model.*
import chat.simplex.app.model.NtfManager.getUserIdFromIntent import chat.simplex.app.model.NtfManager.Companion.getUserIdFromIntent
import chat.simplex.app.ui.theme.* 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
@@ -32,18 +36,12 @@ 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.helpers.DatabaseUtils.ksAppPassword
import chat.simplex.app.views.helpers.DatabaseUtils.ksSelfDestructPassword
import chat.simplex.app.views.localauth.SetAppPasscodeView 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 chat.simplex.app.views.usersettings.LAMode import chat.simplex.app.views.usersettings.LAMode
import chat.simplex.app.views.usersettings.SetDeliveryReceiptsView
import chat.simplex.res.MR
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import java.lang.ref.WeakReference
class MainActivity: FragmentActivity() { class MainActivity: FragmentActivity() {
companion object { companion object {
@@ -63,14 +61,12 @@ class MainActivity: FragmentActivity() {
} }
} }
private val vm by viewModels<SimplexViewModel>() private val vm by viewModels<SimplexViewModel>()
private val destroyedAfterBackPress = mutableStateOf(false)
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
applyAppLocale(ChatModel.controller.appPrefs.appLanguage)
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
SimplexApp.context.mainActivity = WeakReference(this)
// 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) {
@@ -87,17 +83,14 @@ class MainActivity: FragmentActivity() {
} }
setContent { setContent {
SimpleXTheme { SimpleXTheme {
Surface(color = MaterialTheme.colors.background) { MainPage(
MainPage( m,
m, userAuthorized,
userAuthorized, laFailed,
laFailed, ::runAuthenticate,
destroyedAfterBackPress, ::setPerformLA,
::runAuthenticate, showLANotice = { showLANotice(m.controller.appPrefs.laNoticeShown, this) }
::setPerformLA, )
showLANotice = { showLANotice(m.controller.appPrefs.laNoticeShown) }
)
}
} }
} }
SimplexApp.context.schedulePeriodicServiceRestartWorker() SimplexApp.context.schedulePeriodicServiceRestartWorker()
@@ -115,12 +108,7 @@ class MainActivity: FragmentActivity() {
val enteredBackgroundVal = enteredBackground.value val enteredBackgroundVal = enteredBackground.value
val delay = vm.chatModel.controller.appPrefs.laLockDelay.get() val delay = vm.chatModel.controller.appPrefs.laLockDelay.get()
if (enteredBackgroundVal == null || elapsedRealtime() - enteredBackgroundVal >= delay * 1000) { if (enteredBackgroundVal == null || elapsedRealtime() - enteredBackgroundVal >= delay * 1000) {
if (userAuthorized.value != false) { runAuthenticate()
/** [runAuthenticate] will be called in [MainPage] if needed. Making like this prevents double showing of passcode on start */
setAuthState()
} else if (!vm.chatModel.activeCallViewIsVisible.value) {
runAuthenticate()
}
} }
} }
@@ -154,7 +142,6 @@ class MainActivity: FragmentActivity() {
// When pressed Back and there is no one wants to process the back event, clear auth state to force re-auth on launch // When pressed Back and there is no one wants to process the back event, clear auth state to force re-auth on launch
clearAuthState() clearAuthState()
laFailed.value = true laFailed.value = true
destroyedAfterBackPress.value = true
} }
if (!onBackPressedDispatcher.hasEnabledCallbacks()) { if (!onBackPressedDispatcher.hasEnabledCallbacks()) {
// Drop shared content // Drop shared content
@@ -162,28 +149,27 @@ class MainActivity: FragmentActivity() {
} }
} }
private fun setAuthState() {
userAuthorized.value = !vm.chatModel.controller.appPrefs.performLA.get()
}
private fun runAuthenticate() { private fun runAuthenticate() {
val m = vm.chatModel val m = vm.chatModel
setAuthState() if (!m.controller.appPrefs.performLA.get()) {
if (userAuthorized.value == false) { userAuthorized.value = true
} else {
userAuthorized.value = false
ModalManager.shared.closeModals()
// 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 // 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
CoroutineScope(Dispatchers.Default).launch { CoroutineScope(Dispatchers.Default).launch {
delay(50) delay(50)
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
authenticate( authenticate(
if (m.controller.appPrefs.laMode.get() == LAMode.SYSTEM) if (m.controller.appPrefs.laMode.get() == LAMode.SYSTEM)
generalGetString(MR.strings.auth_unlock) generalGetString(R.string.auth_unlock)
else else
generalGetString(MR.strings.la_enter_app_passcode), generalGetString(R.string.la_enter_app_passcode),
if (m.controller.appPrefs.laMode.get() == LAMode.SYSTEM) if (m.controller.appPrefs.laMode.get() == LAMode.SYSTEM)
generalGetString(MR.strings.auth_log_in_using_credential) generalGetString(R.string.auth_log_in_using_credential)
else else
generalGetString(MR.strings.auth_unlock), generalGetString(R.string.auth_unlock),
selfDestruct = true, this@MainActivity,
completed = { laResult -> completed = { laResult ->
when (laResult) { when (laResult) {
LAResult.Success -> LAResult.Success ->
@@ -209,49 +195,50 @@ class MainActivity: FragmentActivity() {
} }
} }
private fun showLANotice(laNoticeShown: SharedPreference<Boolean>) { private fun showLANotice(laNoticeShown: SharedPreference<Boolean>, activity: FragmentActivity) {
Log.d(TAG, "showLANotice") Log.d(TAG, "showLANotice")
if (!laNoticeShown.get()) { if (!laNoticeShown.get()) {
laNoticeShown.set(true) laNoticeShown.set(true)
AlertManager.shared.showAlertDialog( AlertManager.shared.showAlertDialog(
title = generalGetString(MR.strings.la_notice_title_simplex_lock), title = generalGetString(R.string.la_notice_title_simplex_lock),
text = generalGetString(MR.strings.la_notice_to_protect_your_information_turn_on_simplex_lock_you_will_be_prompted_to_complete_authentication_before_this_feature_is_enabled), 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(MR.strings.la_notice_turn_on), confirmText = generalGetString(R.string.la_notice_turn_on),
onConfirm = { onConfirm = {
withBGApi { // to remove this call, change ordering of onConfirm call in AlertManager withBGApi { // to remove this call, change ordering of onConfirm call in AlertManager
showChooseLAMode(laNoticeShown) showChooseLAMode(laNoticeShown, activity)
} }
} }
) )
} }
} }
private fun showChooseLAMode(laNoticeShown: SharedPreference<Boolean>) { private fun showChooseLAMode(laNoticeShown: SharedPreference<Boolean>, activity: FragmentActivity) {
Log.d(TAG, "showLANotice") Log.d(TAG, "showLANotice")
laNoticeShown.set(true) laNoticeShown.set(true)
AlertManager.shared.showAlertDialogStacked( AlertManager.shared.showAlertDialogStacked(
title = generalGetString(MR.strings.la_lock_mode), title = generalGetString(R.string.la_lock_mode),
text = null, text = null,
confirmText = generalGetString(MR.strings.la_lock_mode_passcode), confirmText = generalGetString(R.string.la_lock_mode_passcode),
dismissText = generalGetString(MR.strings.la_lock_mode_system), dismissText = generalGetString(R.string.la_lock_mode_system),
onConfirm = { onConfirm = {
AlertManager.shared.hideAlert() AlertManager.shared.hideAlert()
setPasscode() setPasscode()
}, },
onDismiss = { onDismiss = {
AlertManager.shared.hideAlert() AlertManager.shared.hideAlert()
initialEnableLA() initialEnableLA(activity)
} }
) )
} }
private fun initialEnableLA() { private fun initialEnableLA(activity: FragmentActivity) {
val m = vm.chatModel val m = vm.chatModel
val appPrefs = m.controller.appPrefs val appPrefs = m.controller.appPrefs
m.controller.appPrefs.laMode.set(LAMode.SYSTEM) m.controller.appPrefs.laMode.set(LAMode.SYSTEM)
authenticate( authenticate(
generalGetString(MR.strings.auth_enable_simplex_lock), generalGetString(R.string.auth_enable_simplex_lock),
generalGetString(MR.strings.auth_confirm_credential), generalGetString(R.string.auth_confirm_credential),
activity,
completed = { laResult -> completed = { laResult ->
when (laResult) { when (laResult) {
LAResult.Success -> { LAResult.Success -> {
@@ -292,32 +279,32 @@ class MainActivity: FragmentActivity() {
appPrefs.performLA.set(false) appPrefs.performLA.set(false)
laPasscodeNotSetAlert() laPasscodeNotSetAlert()
}, },
close = close close)
)
} }
} }
} }
private fun setPerformLA(on: Boolean) { private fun setPerformLA(on: Boolean, activity: FragmentActivity) {
vm.chatModel.controller.appPrefs.laNoticeShown.set(true) vm.chatModel.controller.appPrefs.laNoticeShown.set(true)
if (on) { if (on) {
enableLA() enableLA(activity)
} else { } else {
disableLA() disableLA(activity)
} }
} }
private fun enableLA() { private fun enableLA(activity: FragmentActivity) {
val m = vm.chatModel val m = vm.chatModel
authenticate( authenticate(
if (m.controller.appPrefs.laMode.get() == LAMode.SYSTEM) if (m.controller.appPrefs.laMode.get() == LAMode.SYSTEM)
generalGetString(MR.strings.auth_enable_simplex_lock) generalGetString(R.string.auth_enable_simplex_lock)
else else
generalGetString(MR.strings.new_passcode), generalGetString(R.string.new_passcode),
if (m.controller.appPrefs.laMode.get() == LAMode.SYSTEM) if (m.controller.appPrefs.laMode.get() == LAMode.SYSTEM)
generalGetString(MR.strings.auth_confirm_credential) generalGetString(R.string.auth_confirm_credential)
else else
"", "",
activity,
completed = { laResult -> completed = { laResult ->
val prefPerformLA = m.controller.appPrefs.performLA val prefPerformLA = m.controller.appPrefs.performLA
when (laResult) { when (laResult) {
@@ -342,27 +329,25 @@ class MainActivity: FragmentActivity() {
) )
} }
private fun disableLA() { private fun disableLA(activity: FragmentActivity) {
val m = vm.chatModel val m = vm.chatModel
authenticate( authenticate(
if (m.controller.appPrefs.laMode.get() == LAMode.SYSTEM) if (m.controller.appPrefs.laMode.get() == LAMode.SYSTEM)
generalGetString(MR.strings.auth_disable_simplex_lock) generalGetString(R.string.auth_disable_simplex_lock)
else else
generalGetString(MR.strings.la_enter_app_passcode), generalGetString(R.string.la_enter_app_passcode),
if (m.controller.appPrefs.laMode.get() == LAMode.SYSTEM) if (m.controller.appPrefs.laMode.get() == LAMode.SYSTEM)
generalGetString(MR.strings.auth_confirm_credential) generalGetString(R.string.auth_confirm_credential)
else else
generalGetString(MR.strings.auth_disable_simplex_lock), generalGetString(R.string.auth_disable_simplex_lock),
activity,
completed = { laResult -> completed = { laResult ->
val prefPerformLA = m.controller.appPrefs.performLA val prefPerformLA = m.controller.appPrefs.performLA
val selfDestructPref = m.controller.appPrefs.selfDestruct
when (laResult) { when (laResult) {
LAResult.Success -> { LAResult.Success -> {
m.performLA.value = false m.performLA.value = false
prefPerformLA.set(false) prefPerformLA.set(false)
ksAppPassword.remove() ksAppPassword.remove()
selfDestructPref.set(false)
ksSelfDestructPassword.remove()
} }
is LAResult.Failed -> { /* Can be called multiple times on every failure */ } is LAResult.Failed -> { /* Can be called multiple times on every failure */ }
is LAResult.Error -> { is LAResult.Error -> {
@@ -391,9 +376,8 @@ fun MainPage(
chatModel: ChatModel, chatModel: ChatModel,
userAuthorized: MutableState<Boolean?>, userAuthorized: MutableState<Boolean?>,
laFailed: MutableState<Boolean>, laFailed: MutableState<Boolean>,
destroyedAfterBackPress: MutableState<Boolean>,
runAuthenticate: () -> Unit, runAuthenticate: () -> Unit,
setPerformLA: (Boolean) -> Unit, setPerformLA: (Boolean, FragmentActivity) -> Unit,
showLANotice: () -> Unit showLANotice: () -> Unit
) { ) {
var showChatDatabaseError by rememberSaveable { var showChatDatabaseError by rememberSaveable {
@@ -428,38 +412,43 @@ fun MainPage(
} }
@Composable @Composable
fun AuthView() { fun authView() {
Surface(color = MaterialTheme.colors.background) { Box(
Box( Modifier.fillMaxSize().background(MaterialTheme.colors.background),
Modifier.fillMaxSize(), contentAlignment = Alignment.Center
contentAlignment = Alignment.Center ) {
) { SimpleButton(
SimpleButton( stringResource(R.string.auth_unlock),
stringResource(MR.strings.auth_unlock), icon = painterResource(R.drawable.ic_lock),
icon = painterResource(MR.images.ic_lock), click = {
click = { laFailed.value = false
laFailed.value = false runAuthenticate()
runAuthenticate() }
} )
)
}
} }
} }
Box { Box {
val onboarding = chatModel.onboardingStage.value val onboarding = chatModel.onboardingStage.value
val userCreated = chatModel.userCreated.value val userCreated = chatModel.userCreated.value
var showInitializationView by remember { mutableStateOf(false) }
when { when {
chatModel.chatDbStatus.value == null && showInitializationView -> InitializationView()
showChatDatabaseError -> { showChatDatabaseError -> {
chatModel.chatDbStatus.value?.let { chatModel.chatDbStatus.value?.let {
DatabaseErrorView(chatModel.chatDbStatus, chatModel.controller.appPrefs) DatabaseErrorView(chatModel.chatDbStatus, chatModel.controller.appPrefs)
} }
} }
onboarding == null || userCreated == null -> SplashView() onboarding == null || userCreated == null -> SplashView()
userAuthorized.value != true -> {
if (chatModel.controller.appPrefs.performLA.get() && laFailed.value) {
authView()
} else {
SplashView()
}
}
onboarding == OnboardingStage.OnboardingComplete && userCreated -> { onboarding == OnboardingStage.OnboardingComplete && userCreated -> {
Box { Box {
if (chatModel.showCallView.value) ActiveCallView(chatModel)
else {
showAdvertiseLAAlert = true showAdvertiseLAAlert = true
BoxWithConstraints { BoxWithConstraints {
var currentChatId by rememberSaveable { mutableStateOf(chatModel.chatId.value) } var currentChatId by rememberSaveable { mutableStateOf(chatModel.chatId.value) }
@@ -470,15 +459,11 @@ fun MainPage(
translationX = -offset.value.dp.toPx() translationX = -offset.value.dp.toPx()
} }
) { ) {
if (chatModel.setDeliveryReceipts.value) {
SetDeliveryReceiptsView(chatModel)
} else {
val stopped = chatModel.chatRunning.value == false val stopped = chatModel.chatRunning.value == false
if (chatModel.sharedContent.value == null) if (chatModel.sharedContent.value == null)
ChatListView(chatModel, setPerformLA, stopped) ChatListView(chatModel, setPerformLA, stopped)
else else
ShareListView(chatModel, stopped) ShareListView(chatModel, stopped)
}
} }
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val onComposed: () -> Unit = { val onComposed: () -> Unit = {
@@ -509,41 +494,16 @@ 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_CreateSimpleXAddress -> CreateSimpleXAddress(chatModel) onboarding == OnboardingStage.Step3_SetNotificationsMode -> SetNotificationsMode(chatModel)
onboarding == OnboardingStage.Step4_SetNotificationsMode -> SetNotificationsMode(chatModel)
} }
ModalManager.shared.showInView() ModalManager.shared.showInView()
val unauthorized = remember { derivedStateOf { userAuthorized.value != true } }
if (unauthorized.value && !(chatModel.activeCallViewIsVisible.value && chatModel.showCallView.value)) {
LaunchedEffect(Unit) {
// With these constrains when user presses back button while on ChatList, activity destroys and shows auth request
// while the screen moves to a launcher. Detect it and prevent showing the auth
if (!(destroyedAfterBackPress.value && chatModel.controller.appPrefs.laMode.get() == LAMode.SYSTEM)) {
runAuthenticate()
}
}
if (chatModel.controller.appPrefs.performLA.get() && laFailed.value) {
AuthView()
} else {
SplashView()
}
} else if (chatModel.showCallView.value) {
ActiveCallView(chatModel)
}
ModalManager.shared.showPasscodeInView()
val invitation = chatModel.activeCallInvitation.value val invitation = chatModel.activeCallInvitation.value
if (invitation != null) IncomingCallAlertView(invitation, chatModel) if (invitation != null) IncomingCallAlertView(invitation, chatModel)
AlertManager.shared.showInView() AlertManager.shared.showInView()
LaunchedEffect(Unit) {
delay(1000)
if (chatModel.chatDbStatus.value == null) {
showInitializationView = true
}
}
} }
DisposableEffectOnRotate { DisposableEffectOnRotate {
@@ -555,22 +515,6 @@ fun MainPage(
} }
} }
@Composable
private fun InitializationView() {
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
CircularProgressIndicator(
Modifier
.padding(bottom = DEFAULT_PADDING)
.size(30.dp),
color = MaterialTheme.colors.secondary,
strokeWidth = 2.5.dp
)
Text(stringResource(MR.strings.opening_database))
}
}
}
fun processNotificationIntent(intent: Intent?, chatModel: ChatModel) { fun processNotificationIntent(intent: Intent?, chatModel: ChatModel) {
val userId = getUserIdFromIntent(intent) val userId = getUserIdFromIntent(intent)
when (intent?.action) { when (intent?.action) {
@@ -585,7 +529,7 @@ fun processNotificationIntent(intent: Intent?, chatModel: ChatModel) {
} }
val cInfo = chatModel.getChat(chatId)?.chatInfo val cInfo = chatModel.getChat(chatId)?.chatInfo
chatModel.clearOverlays.value = true chatModel.clearOverlays.value = true
if (cInfo != null && (cInfo is ChatInfo.Direct || cInfo is ChatInfo.Group)) openChat(cInfo, chatModel) if (cInfo != null) openChat(cInfo, chatModel)
} }
} }
} }
@@ -607,7 +551,7 @@ fun processNotificationIntent(intent: Intent?, chatModel: ChatModel) {
chatModel.clearOverlays.value = true chatModel.clearOverlays.value = true
val invitation = chatModel.callInvitations[chatId] val invitation = chatModel.callInvitations[chatId]
if (invitation == null) { if (invitation == null) {
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.call_already_ended)) AlertManager.shared.showAlertMsg(generalGetString(R.string.call_already_ended))
} else { } else {
chatModel.callManager.acceptIncomingCall(invitation = invitation) chatModel.callManager.acceptIncomingCall(invitation = invitation)
} }
@@ -679,17 +623,17 @@ fun connectIfOpenedViaUri(uri: Uri, chatModel: ChatModel) {
} else { } else {
withUriAction(uri) { linkType -> withUriAction(uri) { linkType ->
val title = when (linkType) { val title = when (linkType) {
ConnectionLinkType.CONTACT -> generalGetString(MR.strings.connect_via_contact_link) ConnectionLinkType.CONTACT -> generalGetString(R.string.connect_via_contact_link)
ConnectionLinkType.INVITATION -> generalGetString(MR.strings.connect_via_invitation_link) ConnectionLinkType.INVITATION -> generalGetString(R.string.connect_via_invitation_link)
ConnectionLinkType.GROUP -> generalGetString(MR.strings.connect_via_group_link) ConnectionLinkType.GROUP -> generalGetString(R.string.connect_via_group_link)
} }
AlertManager.shared.showAlertDialog( AlertManager.shared.showAlertDialog(
title = title, title = title,
text = if (linkType == ConnectionLinkType.GROUP) text = if (linkType == ConnectionLinkType.GROUP)
generalGetString(MR.strings.you_will_join_group) generalGetString(R.string.you_will_join_group)
else else
generalGetString(MR.strings.profile_will_be_sent_to_contact_sending_link), generalGetString(R.string.profile_will_be_sent_to_contact_sending_link),
confirmText = generalGetString(MR.strings.connect_via_link_verb), confirmText = generalGetString(R.string.connect_via_link_verb),
onConfirm = { onConfirm = {
withApi { withApi {
Log.d(TAG, "connectIfOpenedViaUri: connecting") Log.d(TAG, "connectIfOpenedViaUri: connecting")
@@ -10,11 +10,9 @@ 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
import com.jakewharton.processphoenix.ProcessPhoenix
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.serialization.decodeFromString import kotlinx.serialization.decodeFromString
import java.io.* import java.io.*
import java.lang.ref.WeakReference
import java.util.* import java.util.*
import java.util.concurrent.Semaphore import java.util.concurrent.Semaphore
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@@ -38,20 +36,15 @@ external fun chatParseServer(str: String): String
external fun chatPasswordHash(pwd: String, salt: String): String external fun chatPasswordHash(pwd: String, salt: String): String
class SimplexApp: Application(), LifecycleEventObserver { class SimplexApp: Application(), LifecycleEventObserver {
var mainActivity: WeakReference<MainActivity> = WeakReference(null) lateinit var chatController: ChatController
val chatModel: ChatModel
get() = chatController.chatModel
val appPreferences: AppPreferences
get() = chatController.appPrefs
val chatController: ChatController = ChatController
var isAppOnForeground: Boolean = false var isAppOnForeground: Boolean = false
val defaultLocale: Locale = Locale.getDefault() val defaultLocale: Locale = Locale.getDefault()
suspend fun initChatController(useKey: String? = null, confirmMigrations: MigrationConfirmation? = null, startChat: Boolean = true) { fun initChatController(useKey: String? = null, confirmMigrations: MigrationConfirmation? = null, startChat: Boolean = true) {
val dbKey = useKey ?: DatabaseUtils.useDatabaseKey() val dbKey = useKey ?: DatabaseUtils.useDatabaseKey()
val dbAbsolutePathPrefix = getFilesDirectory() val dbAbsolutePathPrefix = getFilesDirectory(SimplexApp.context)
val confirm = confirmMigrations ?: if (appPreferences.confirmDBUpgrades.get()) MigrationConfirmation.Error else MigrationConfirmation.YesUp val confirm = confirmMigrations ?: if (appPreferences.confirmDBUpgrades.get()) MigrationConfirmation.Error else MigrationConfirmation.YesUp
val migrated: Array<Any> = chatMigrateInit(dbAbsolutePathPrefix, dbKey, confirm.value) val migrated: Array<Any> = chatMigrateInit(dbAbsolutePathPrefix, dbKey, confirm.value)
val res: DBMigrationResult = kotlin.runCatching { val res: DBMigrationResult = kotlin.runCatching {
@@ -60,7 +53,11 @@ class SimplexApp: Application(), LifecycleEventObserver {
val ctrl = if (res is DBMigrationResult.OK) { val ctrl = if (res is DBMigrationResult.OK) {
migrated[1] as Long migrated[1] as Long
} else null } else null
chatController.ctrl = ctrl if (::chatController.isInitialized) {
chatController.ctrl = ctrl
} else {
chatController = ChatController(ctrl, ntfManager, applicationContext, appPreferences)
}
chatModel.chatDbEncrypted.value = dbKey != "" chatModel.chatDbEncrypted.value = dbKey != ""
chatModel.chatDbStatus.value = res chatModel.chatDbStatus.value = res
if (res != DBMigrationResult.OK) { if (res != DBMigrationResult.OK) {
@@ -68,47 +65,38 @@ class SimplexApp: Application(), LifecycleEventObserver {
} else if (startChat) { } else if (startChat) {
// If we migrated successfully means previous re-encryption process on database level finished successfully too // If we migrated successfully means previous re-encryption process on database level finished successfully too
if (appPreferences.encryptionStartedAt.get() != null) appPreferences.encryptionStartedAt.set(null) if (appPreferences.encryptionStartedAt.get() != null) appPreferences.encryptionStartedAt.set(null)
val user = chatController.apiGetActiveUser() withApi {
if (user == null) { val user = chatController.apiGetActiveUser()
chatModel.controller.appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo) if (user == null) {
chatModel.controller.appPrefs.privacyDeliveryReceiptsSet.set(true) chatModel.onboardingStage.value = OnboardingStage.Step1_SimpleXInfo
chatModel.onboardingStage.value = OnboardingStage.Step1_SimpleXInfo
chatModel.currentUser.value = null
chatModel.users.clear()
} else {
val savedOnboardingStage = appPreferences.onboardingStage.get()
chatModel.onboardingStage.value = if (listOf(OnboardingStage.Step1_SimpleXInfo, OnboardingStage.Step2_CreateProfile).contains(savedOnboardingStage) && chatModel.users.size == 1) {
OnboardingStage.Step3_CreateSimpleXAddress
} else { } else {
savedOnboardingStage chatController.startChat(user)
} chatController.showBackgroundServiceNoticeIfNeeded()
if (chatModel.onboardingStage.value == OnboardingStage.OnboardingComplete && !chatModel.controller.appPrefs.privacyDeliveryReceiptsSet.get()) {
chatModel.setDeliveryReceipts.value = true
}
chatController.startChat(user)
// Prevents from showing "Enable notifications" alert when onboarding wasn't complete yet
if (chatModel.onboardingStage.value == OnboardingStage.OnboardingComplete) {
SimplexService.showBackgroundServiceNoticeIfNeeded()
if (appPreferences.notificationsMode.get() == NotificationsMode.SERVICE.name) if (appPreferences.notificationsMode.get() == NotificationsMode.SERVICE.name)
SimplexService.start() SimplexService.start(applicationContext)
} }
} }
} }
} }
val chatModel: ChatModel
get() = chatController.chatModel
private val ntfManager: NtfManager by lazy {
NtfManager(applicationContext, appPreferences)
}
private val appPreferences: AppPreferences by lazy {
AppPreferences(applicationContext)
}
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
if (ProcessPhoenix.isPhoenixProcess(this)) {
return;
}
context = this context = this
initChatController()
ProcessLifecycleOwner.get().lifecycle.addObserver(this)
context.getDir("temp", MODE_PRIVATE).deleteRecursively() context.getDir("temp", MODE_PRIVATE).deleteRecursively()
withBGApi { runMigrations()
initChatController()
runMigrations()
}
ProcessLifecycleOwner.get().lifecycle.addObserver(this@SimplexApp)
} }
override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) { override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
@@ -138,7 +126,7 @@ class SimplexApp: Application(), LifecycleEventObserver {
Lifecycle.Event.ON_RESUME -> { Lifecycle.Event.ON_RESUME -> {
isAppOnForeground = true isAppOnForeground = true
if (chatModel.onboardingStage.value == OnboardingStage.OnboardingComplete) { if (chatModel.onboardingStage.value == OnboardingStage.OnboardingComplete) {
SimplexService.showBackgroundServiceNoticeIfNeeded() chatController.showBackgroundServiceNoticeIfNeeded()
} }
/** /**
* We're starting service here instead of in [Lifecycle.Event.ON_START] because * We're starting service here instead of in [Lifecycle.Event.ON_START] because
@@ -149,7 +137,7 @@ class SimplexApp: Application(), LifecycleEventObserver {
chatModel.onboardingStage.value == OnboardingStage.OnboardingComplete && chatModel.onboardingStage.value == OnboardingStage.OnboardingComplete &&
appPreferences.notificationsMode.get() == NotificationsMode.SERVICE.name appPreferences.notificationsMode.get() == NotificationsMode.SERVICE.name
) { ) {
SimplexService.start() SimplexService.start(applicationContext)
} }
} }
else -> isAppOnForeground = false else -> isAppOnForeground = false
@@ -159,12 +147,12 @@ class SimplexApp: Application(), LifecycleEventObserver {
fun allowToStartServiceAfterAppExit() = with(chatModel.controller) { fun allowToStartServiceAfterAppExit() = with(chatModel.controller) {
appPrefs.notificationsMode.get() == NotificationsMode.SERVICE.name && appPrefs.notificationsMode.get() == NotificationsMode.SERVICE.name &&
(!NotificationsMode.SERVICE.requiresIgnoringBattery || SimplexService.isIgnoringBatteryOptimizations()) (!NotificationsMode.SERVICE.requiresIgnoringBattery || isIgnoringBatteryOptimizations(chatModel.controller.appContext))
} }
private fun allowToStartPeriodically() = with(chatModel.controller) { private fun allowToStartPeriodically() = with(chatModel.controller) {
appPrefs.notificationsMode.get() == NotificationsMode.PERIODIC.name && appPrefs.notificationsMode.get() == NotificationsMode.PERIODIC.name &&
(!NotificationsMode.PERIODIC.requiresIgnoringBattery || SimplexService.isIgnoringBatteryOptimizations()) (!NotificationsMode.PERIODIC.requiresIgnoringBattery || isIgnoringBatteryOptimizations(chatModel.controller.appContext))
} }
/* /*
@@ -1,29 +1,17 @@
package chat.simplex.app package chat.simplex.app
import android.annotation.SuppressLint
import android.app.* import android.app.*
import android.content.* import android.content.*
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.net.Uri
import android.os.* import android.os.*
import android.provider.Settings import android.provider.Settings
import android.util.Log import android.util.Log
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.work.* import androidx.work.*
import chat.simplex.app.model.ChatController
import chat.simplex.app.model.ChatModel
import chat.simplex.app.views.helpers.* import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.usersettings.NotificationsMode import kotlinx.coroutines.Dispatchers
import chat.simplex.res.MR import kotlinx.coroutines.withContext
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
import kotlinx.coroutines.*
// based on: // based on:
// https://robertohuertas.com/2019/06/29/android_foreground_services/ // https://robertohuertas.com/2019/06/29/android_foreground_services/
@@ -53,8 +41,8 @@ class SimplexService: Service() {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
Log.d(TAG, "Simplex service created") Log.d(TAG, "Simplex service created")
val title = generalGetString(MR.strings.simplex_service_notification_title) val title = getString(R.string.simplex_service_notification_title)
val text = generalGetString(MR.strings.simplex_service_notification_text) val text = getString(R.string.simplex_service_notification_text)
notificationManager = createNotificationChannel() notificationManager = createNotificationChannel()
serviceNotification = createNotification(title, text) serviceNotification = createNotification(title, text)
startForeground(SIMPLEX_SERVICE_ID, serviceNotification) startForeground(SIMPLEX_SERVICE_ID, serviceNotification)
@@ -98,7 +86,6 @@ class SimplexService: Service() {
isStartingService = true isStartingService = true
withApi { withApi {
val chatController = (application as SimplexApp).chatController val chatController = (application as SimplexApp).chatController
waitDbMigrationEnds(chatController)
try { try {
Log.w(TAG, "Starting foreground service") Log.w(TAG, "Starting foreground service")
val chatDbStatus = chatController.chatModel.chatDbStatus.value val chatDbStatus = chatController.chatModel.chatDbStatus.value
@@ -154,7 +141,7 @@ class SimplexService: Service() {
setupIntent.putExtra(Settings.EXTRA_APP_PACKAGE, packageName) setupIntent.putExtra(Settings.EXTRA_APP_PACKAGE, packageName)
setupIntent.putExtra(Settings.EXTRA_CHANNEL_ID, NOTIFICATION_CHANNEL_ID) setupIntent.putExtra(Settings.EXTRA_CHANNEL_ID, NOTIFICATION_CHANNEL_ID)
val setup = PendingIntent.getActivity(this, 0, setupIntent, flags) val setup = PendingIntent.getActivity(this, 0, setupIntent, flags)
builder.addAction(0, generalGetString(MR.strings.hide_notification), setup) builder.addAction(0, getString(R.string.hide_notification), setup)
} }
return builder.build() return builder.build()
@@ -178,6 +165,7 @@ class SimplexService: Service() {
it.setPackage(packageName) it.setPackage(packageName)
}; };
val restartServicePendingIntent: PendingIntent = PendingIntent.getService(this, 1, restartServiceIntent, PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE); val restartServicePendingIntent: PendingIntent = PendingIntent.getService(this, 1, restartServiceIntent, PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE);
applicationContext.getSystemService(Context.ALARM_SERVICE);
val alarmService: AlarmManager = applicationContext.getSystemService(Context.ALARM_SERVICE) as AlarmManager; val alarmService: AlarmManager = applicationContext.getSystemService(Context.ALARM_SERVICE) as AlarmManager;
alarmService.set(AlarmManager.ELAPSED_REALTIME, SystemClock.elapsedRealtime() + 1000, restartServicePendingIntent); alarmService.set(AlarmManager.ELAPSED_REALTIME, SystemClock.elapsedRealtime() + 1000, restartServicePendingIntent);
} }
@@ -218,7 +206,7 @@ class SimplexService: Service() {
} }
if (getServiceState(context) == ServiceState.STARTED) { if (getServiceState(context) == ServiceState.STARTED) {
Log.d(TAG, "ServiceStartWorker: Starting foreground service (work ID: $id)") Log.d(TAG, "ServiceStartWorker: Starting foreground service (work ID: $id)")
start() start(context)
} }
return Result.success() return Result.success()
} }
@@ -259,7 +247,7 @@ class SimplexService: Service() {
workManager.enqueueUniqueWork(WORK_NAME_ONCE, ExistingWorkPolicy.KEEP, startServiceRequest) // Unique avoids races! workManager.enqueueUniqueWork(WORK_NAME_ONCE, ExistingWorkPolicy.KEEP, startServiceRequest) // Unique avoids races!
} }
suspend fun start() = serviceAction(Action.START) suspend fun start(context: Context) = serviceAction(context, Action.START)
/** /**
* If there is a need to stop the service, use this function only. It makes sure that the service will be stopped without an * If there is a need to stop the service, use this function only. It makes sure that the service will be stopped without an
@@ -273,12 +261,12 @@ class SimplexService: Service() {
} }
} }
private suspend fun serviceAction(action: Action) { private suspend fun serviceAction(context: Context, action: Action) {
Log.d(TAG, "SimplexService serviceAction: ${action.name}") Log.d(TAG, "SimplexService serviceAction: ${action.name}")
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
Intent(SimplexApp.context, SimplexService::class.java).also { Intent(context, SimplexService::class.java).also {
it.action = action.name it.action = action.name
ContextCompat.startForegroundService(SimplexApp.context, it) ContextCompat.startForegroundService(context, it)
} }
} }
} }
@@ -307,15 +295,15 @@ class SimplexService: Service() {
} }
val title = when(chatDbStatus) { val title = when(chatDbStatus) {
is DBMigrationResult.ErrorNotADatabase -> generalGetString(MR.strings.enter_passphrase_notification_title) is DBMigrationResult.ErrorNotADatabase -> generalGetString(R.string.enter_passphrase_notification_title)
is DBMigrationResult.OK -> return is DBMigrationResult.OK -> return
else -> generalGetString(MR.strings.database_initialization_error_title) else -> generalGetString(R.string.database_initialization_error_title)
} }
val description = when(chatDbStatus) { val description = when(chatDbStatus) {
is DBMigrationResult.ErrorNotADatabase -> generalGetString(MR.strings.enter_passphrase_notification_desc) is DBMigrationResult.ErrorNotADatabase -> generalGetString(R.string.enter_passphrase_notification_desc)
is DBMigrationResult.OK -> return is DBMigrationResult.OK -> return
else -> generalGetString(MR.strings.database_initialization_error_desc) else -> generalGetString(R.string.database_initialization_error_desc)
} }
val builder = NotificationCompat.Builder(SimplexApp.context, NOTIFICATION_CHANNEL_ID) val builder = NotificationCompat.Builder(SimplexApp.context, NOTIFICATION_CHANNEL_ID)
@@ -336,172 +324,6 @@ class SimplexService: Service() {
notificationManager.cancel(PASSPHRASE_NOTIFICATION_ID) notificationManager.cancel(PASSPHRASE_NOTIFICATION_ID)
} }
/*
* When the app starts the database is in migration process. It can take from seconds to tens of seconds.
* It happens in background thread, so other places that relies on database should wait til the end of migration
* */
suspend fun waitDbMigrationEnds(chatController: ChatController) {
var maxWaitTime = 50_000
while (chatController.chatModel.chatDbStatus.value == null && maxWaitTime > 0) {
delay(50)
maxWaitTime -= 50
}
}
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)
fun showBackgroundServiceNoticeIfNeeded() {
val appPrefs = ChatController.appPrefs
val mode = NotificationsMode.valueOf(appPrefs.notificationsMode.get()!!)
Log.d(TAG, "showBackgroundServiceNoticeIfNeeded")
// Nothing to do if mode is OFF. Can be selected on on-boarding stage
if (mode == NotificationsMode.OFF) return
if (!appPrefs.backgroundServiceNoticeShown.get()) {
// the branch for the new users who have never seen service notice
if (!mode.requiresIgnoringBattery || isIgnoringBatteryOptimizations()) {
showBGServiceNotice(mode)
} else {
showBGServiceNoticeIgnoreOptimization(mode)
}
// set both flags, so that if the user doesn't allow ignoring optimizations, the service will be disabled without additional notice
appPrefs.backgroundServiceNoticeShown.set(true)
appPrefs.backgroundServiceBatteryNoticeShown.set(true)
} else if (mode.requiresIgnoringBattery && !isIgnoringBatteryOptimizations()) {
// the branch for users who have app installed, and have seen the service notice,
// but the battery optimization for the app is on (Android 12) AND the service is running
if (appPrefs.backgroundServiceBatteryNoticeShown.get()) {
// users have been presented with battery notice before - they did not allow ignoring optimizations -> disable service
showDisablingServiceNotice(mode)
appPrefs.notificationsMode.set(NotificationsMode.OFF.name)
ChatModel.notificationsMode.value = NotificationsMode.OFF
SimplexService.StartReceiver.toggleReceiver(false)
MessagesFetcherWorker.cancelAll()
SimplexService.safeStopService(SimplexApp.context)
} else {
// show battery optimization notice
showBGServiceNoticeIgnoreOptimization(mode)
appPrefs.backgroundServiceBatteryNoticeShown.set(true)
}
} else {
// service or periodic mode was chosen and battery optimization is disabled
SimplexApp.context.schedulePeriodicServiceRestartWorker()
SimplexApp.context.schedulePeriodicWakeUp()
}
}
private fun showBGServiceNotice(mode: NotificationsMode) = AlertManager.shared.showAlert {
AlertDialog(
onDismissRequest = AlertManager.shared::hideAlert,
title = {
Row {
Icon(
painterResource(MR.images.ic_bolt),
contentDescription =
if (mode == NotificationsMode.SERVICE) stringResource(MR.strings.icon_descr_instant_notifications) else stringResource(MR.strings.periodic_notifications),
)
Text(
if (mode == NotificationsMode.SERVICE) stringResource(MR.strings.icon_descr_instant_notifications) else stringResource(MR.strings.periodic_notifications),
fontWeight = FontWeight.Bold
)
}
},
text = {
Column {
Text(
if (mode == NotificationsMode.SERVICE) annotatedStringResource(MR.strings.to_preserve_privacy_simplex_has_background_service_instead_of_push_notifications_it_uses_a_few_pc_battery) else annotatedStringResource(MR.strings.periodic_notifications_desc),
Modifier.padding(bottom = 8.dp)
)
Text(
annotatedStringResource(MR.strings.it_can_disabled_via_settings_notifications_still_shown)
)
}
},
confirmButton = {
TextButton(onClick = AlertManager.shared::hideAlert) { Text(stringResource(MR.strings.ok)) }
}
)
}
private fun showBGServiceNoticeIgnoreOptimization(mode: NotificationsMode) = AlertManager.shared.showAlert {
val ignoreOptimization = {
AlertManager.shared.hideAlert()
askAboutIgnoringBatteryOptimization()
}
AlertDialog(
onDismissRequest = ignoreOptimization,
title = {
Row {
Icon(
painterResource(MR.images.ic_bolt),
contentDescription =
if (mode == NotificationsMode.SERVICE) stringResource(MR.strings.icon_descr_instant_notifications) else stringResource(MR.strings.periodic_notifications),
)
Text(
if (mode == NotificationsMode.SERVICE) stringResource(MR.strings.service_notifications) else stringResource(MR.strings.periodic_notifications),
fontWeight = FontWeight.Bold
)
}
},
text = {
Column {
Text(
if (mode == NotificationsMode.SERVICE) annotatedStringResource(MR.strings.to_preserve_privacy_simplex_has_background_service_instead_of_push_notifications_it_uses_a_few_pc_battery) else annotatedStringResource(MR.strings.periodic_notifications_desc),
Modifier.padding(bottom = 8.dp)
)
Text(annotatedStringResource(MR.strings.turn_off_battery_optimization))
}
},
confirmButton = {
TextButton(onClick = ignoreOptimization) { Text(stringResource(MR.strings.ok)) }
}
)
}
private fun showDisablingServiceNotice(mode: NotificationsMode) = AlertManager.shared.showAlert {
AlertDialog(
onDismissRequest = AlertManager.shared::hideAlert,
title = {
Row {
Icon(
painterResource(MR.images.ic_bolt),
contentDescription =
if (mode == NotificationsMode.SERVICE) stringResource(MR.strings.icon_descr_instant_notifications) else stringResource(MR.strings.periodic_notifications),
)
Text(
if (mode == NotificationsMode.SERVICE) stringResource(MR.strings.service_notifications_disabled) else stringResource(MR.strings.periodic_notifications_disabled),
fontWeight = FontWeight.Bold
)
}
},
text = {
Column {
Text(
annotatedStringResource(MR.strings.turning_off_service_and_periodic),
Modifier.padding(bottom = 8.dp)
)
}
},
confirmButton = {
TextButton(onClick = AlertManager.shared::hideAlert) { Text(stringResource(MR.strings.ok)) }
}
)
}
fun isIgnoringBatteryOptimizations(): Boolean {
val powerManager = SimplexApp.context.getSystemService(Application.POWER_SERVICE) as PowerManager
return powerManager.isIgnoringBatteryOptimizations(SimplexApp.context.packageName)
}
private fun askAboutIgnoringBatteryOptimization() {
Intent().apply {
@SuppressLint("BatteryLife")
action = Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
data = Uri.parse("package:${SimplexApp.context.packageName}")
// This flag is needed when you start a new activity from non-Activity context
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
SimplexApp.context.startActivity(this)
}
}
} }
} }
@@ -1,48 +1,45 @@
package chat.simplex.app.model package chat.simplex.app.model
import android.Manifest
import android.app.* import android.app.*
import android.app.TaskStackBuilder
import android.content.* import android.content.*
import android.content.pm.PackageManager
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import android.hardware.display.DisplayManager import android.hardware.display.DisplayManager
import android.media.AudioAttributes import android.media.AudioAttributes
import android.net.Uri import android.net.Uri
import android.util.Log import android.util.Log
import android.view.Display import android.view.Display
import androidx.core.app.* import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import chat.simplex.app.* import chat.simplex.app.*
import chat.simplex.app.views.call.* import chat.simplex.app.views.call.*
import chat.simplex.app.views.chatlist.acceptContactRequest import chat.simplex.app.views.chatlist.acceptContactRequest
import chat.simplex.app.views.helpers.* import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.usersettings.NotificationPreviewMode import chat.simplex.app.views.usersettings.NotificationPreviewMode
import chat.simplex.res.MR
import kotlinx.datetime.Clock import kotlinx.datetime.Clock
object NtfManager { class NtfManager(val context: Context, private val appPreferences: AppPreferences) {
const val MessageChannel: String = "chat.simplex.app.MESSAGE_NOTIFICATION" companion object {
const val MessageGroup: String = "chat.simplex.app.MESSAGE_NOTIFICATION" const val MessageChannel: String = "chat.simplex.app.MESSAGE_NOTIFICATION"
const val OpenChatAction: String = "chat.simplex.app.OPEN_CHAT" const val MessageGroup: String = "chat.simplex.app.MESSAGE_NOTIFICATION"
const val ShowChatsAction: String = "chat.simplex.app.SHOW_CHATS" const val OpenChatAction: String = "chat.simplex.app.OPEN_CHAT"
const val ShowChatsAction: String = "chat.simplex.app.SHOW_CHATS"
// DO NOT change notification channel settings / names // DO NOT change notification channel settings / names
const val CallChannel: String = "chat.simplex.app.CALL_NOTIFICATION_1" const val CallChannel: String = "chat.simplex.app.CALL_NOTIFICATION_1"
const val AcceptCallAction: String = "chat.simplex.app.ACCEPT_CALL" const val AcceptCallAction: String = "chat.simplex.app.ACCEPT_CALL"
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 val appPreferences: AppPreferences by lazy { ChatController.appPrefs }
private val context: Context
get() = SimplexApp.context
fun getUserIdFromIntent(intent: Intent?): Long? { private const val UserIdKey: String = "userId"
val userId = intent?.getLongExtra(UserIdKey, -1L) private const val ChatIdKey: String = "chatId"
return if (userId == -1L || userId == null) null else userId
fun getUserIdFromIntent(intent: Intent?): Long? {
val userId = intent?.getLongExtra(UserIdKey, -1L)
return if (userId == -1L || userId == null) null else userId
}
} }
private val manager: NotificationManager = SimplexApp.context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager private val manager: NotificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
private var prevNtfTime = mutableMapOf<String, Long>() private var prevNtfTime = mutableMapOf<String, Long>()
private val msgNtfTimeoutMs = 30000L private val msgNtfTimeoutMs = 30000L
@@ -61,7 +58,7 @@ object NtfManager {
.setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE) .setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
.build() .build()
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)
Log.d(TAG, "callNotificationChannel sound: $soundUri") Log.d(TAG,"callNotificationChannel sound: $soundUri")
callChannel.setSound(soundUri, attrs) callChannel.setSound(soundUri, attrs)
callChannel.enableVibration(true) callChannel.enableVibration(true)
// the numbers below are explained here: https://developer.android.com/reference/android/os/Vibrator // the numbers below are explained here: https://developer.android.com/reference/android/os/Vibrator
@@ -73,8 +70,8 @@ object NtfManager {
fun cancelNotificationsForChat(chatId: String) { fun cancelNotificationsForChat(chatId: String) {
prevNtfTime.remove(chatId) prevNtfTime.remove(chatId)
manager.cancel(chatId.hashCode()) manager.cancel(chatId.hashCode())
val msgNtfs = manager.activeNotifications.filter { ntf -> val msgNtfs = manager.activeNotifications.filter {
ntf.notification.channelId == MessageChannel ntf -> ntf.notification.channelId == MessageChannel
} }
if (msgNtfs.count() == 1) { if (msgNtfs.count() == 1) {
// Have a group notification with no children so cancel it // Have a group notification with no children so cancel it
@@ -87,7 +84,7 @@ object NtfManager {
user = user, user = user,
chatId = cInfo.id, chatId = cInfo.id,
displayName = cInfo.displayName, displayName = cInfo.displayName,
msgText = generalGetString(MR.strings.notification_new_contact_request), msgText = generalGetString(R.string.notification_new_contact_request),
image = cInfo.image, image = cInfo.image,
listOf(NotificationAction.ACCEPT_CONTACT_REQUEST) listOf(NotificationAction.ACCEPT_CONTACT_REQUEST)
) )
@@ -98,7 +95,7 @@ object NtfManager {
user = user, user = user,
chatId = contact.id, chatId = contact.id,
displayName = contact.displayName, displayName = contact.displayName,
msgText = generalGetString(MR.strings.notification_contact_connected) msgText = generalGetString(R.string.notification_contact_connected)
) )
} }
@@ -113,9 +110,10 @@ object NtfManager {
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)
prevNtfTime[chatId] = now prevNtfTime[chatId] = now
val previewMode = appPreferences.notificationPreviewMode.get() val previewMode = appPreferences.notificationPreviewMode.get()
val title = if (previewMode == NotificationPreviewMode.HIDDEN.name) generalGetString(MR.strings.notification_preview_somebody) else displayName val title = if (previewMode == NotificationPreviewMode.HIDDEN.name) generalGetString(R.string.notification_preview_somebody) else displayName
val content = if (previewMode != NotificationPreviewMode.MESSAGE.name) generalGetString(MR.strings.notification_preview_new_message) else msgText val content = if (previewMode != NotificationPreviewMode.MESSAGE.name) generalGetString(R.string.notification_preview_new_message) else msgText
val largeIcon = when { val largeIcon = when {
actions.isEmpty() -> null actions.isEmpty() -> null
image == null || previewMode == NotificationPreviewMode.HIDDEN.name -> BitmapFactory.decodeResource(context.resources, R.drawable.icon) image == null || previewMode == NotificationPreviewMode.HIDDEN.name -> BitmapFactory.decodeResource(context.resources, R.drawable.icon)
@@ -143,10 +141,11 @@ object NtfManager {
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) {
NotificationAction.ACCEPT_CONTACT_REQUEST -> generalGetString(MR.strings.accept) NotificationAction.ACCEPT_CONTACT_REQUEST -> generalGetString(R.string.accept)
} }
builder.addAction(0, actionButton, actionPendingIntent) builder.addAction(0, actionButton, actionPendingIntent)
} }
val summary = NotificationCompat.Builder(context, MessageChannel) val summary = NotificationCompat.Builder(context, MessageChannel)
.setSmallIcon(R.drawable.ntf_icon) .setSmallIcon(R.drawable.ntf_icon)
.setColor(0x88FFFF) .setColor(0x88FFFF)
@@ -158,17 +157,14 @@ object NtfManager {
with(NotificationManagerCompat.from(context)) { with(NotificationManagerCompat.from(context)) {
// using cInfo.id only shows one notification per chat and updates it when the message arrives // using cInfo.id only shows one notification per chat and updates it when the message arrives
if (ActivityCompat.checkSelfPermission(SimplexApp.context, Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED) { notify(chatId.hashCode(), builder.build())
notify(chatId.hashCode(), builder.build()) notify(0, summary)
notify(0, summary)
}
} }
} }
fun notifyCallInvitation(invitation: RcvCallInvitation) { fun notifyCallInvitation(invitation: RcvCallInvitation) {
val keyguardManager = getKeyguardManager(context) val keyguardManager = getKeyguardManager(context)
Log.d( Log.d(TAG,
TAG,
"notifyCallInvitation pre-requests: " + "notifyCallInvitation pre-requests: " +
"keyguard locked ${keyguardManager.isKeyguardLocked}, " + "keyguard locked ${keyguardManager.isKeyguardLocked}, " +
"callOnLockScreen ${appPreferences.callOnLockScreen.get()}, " + "callOnLockScreen ${appPreferences.callOnLockScreen.get()}, " +
@@ -192,21 +188,21 @@ object NtfManager {
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.user.userId, invitation.contact.id)) .setContentIntent(chatPendingIntent(OpenChatAction, invitation.user.userId, invitation.contact.id))
.addAction(R.drawable.ntf_icon, generalGetString(MR.strings.accept), chatPendingIntent(AcceptCallAction, invitation.user.userId, contactId)) .addAction(R.drawable.ntf_icon, generalGetString(R.string.accept), chatPendingIntent(AcceptCallAction, invitation.user.userId, contactId))
.addAction(R.drawable.ntf_icon, generalGetString(MR.strings.reject), chatPendingIntent(RejectCallAction, invitation.user.userId, 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)
} }
val text = generalGetString( val text = generalGetString(
if (invitation.callType.media == CallMediaType.Video) { if (invitation.callType.media == CallMediaType.Video) {
if (invitation.sharedKey == null) MR.strings.video_call_no_encryption else MR.strings.encrypted_video_call if (invitation.sharedKey == null) R.string.video_call_no_encryption else R.string.encrypted_video_call
} else { } else {
if (invitation.sharedKey == null) MR.strings.audio_call_no_encryption else MR.strings.encrypted_audio_call if (invitation.sharedKey == null) R.string.audio_call_no_encryption else R.string.encrypted_audio_call
} }
) )
val previewMode = appPreferences.notificationPreviewMode.get() val previewMode = appPreferences.notificationPreviewMode.get()
val title = if (previewMode == NotificationPreviewMode.HIDDEN.name) val title = if (previewMode == NotificationPreviewMode.HIDDEN.name)
generalGetString(MR.strings.notification_preview_somebody) generalGetString(R.string.notification_preview_somebody)
else else
invitation.contact.displayName invitation.contact.displayName
val largeIcon = if (image == null || previewMode == NotificationPreviewMode.HIDDEN.name) val largeIcon = if (image == null || previewMode == NotificationPreviewMode.HIDDEN.name)
@@ -227,9 +223,7 @@ object NtfManager {
// This makes notification sound and vibration repeat endlessly // This makes notification sound and vibration repeat endlessly
notification.flags = notification.flags or NotificationCompat.FLAG_INSISTENT notification.flags = notification.flags or NotificationCompat.FLAG_INSISTENT
with(NotificationManagerCompat.from(context)) { with(NotificationManagerCompat.from(context)) {
if (ActivityCompat.checkSelfPermission(SimplexApp.context, Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED) { notify(CallNotificationId, notification)
notify(CallNotificationId, notification)
}
} }
} }
@@ -237,13 +231,9 @@ object NtfManager {
manager.cancel(CallNotificationId) manager.cancel(CallNotificationId)
} }
fun cancelAllNotifications() {
manager.cancelAll()
}
fun hasNotificationsForChat(chatId: String): Boolean = manager.activeNotifications.any { it.id == chatId.hashCode() } fun hasNotificationsForChat(chatId: String): Boolean = manager.activeNotifications.any { it.id == chatId.hashCode() }
private fun hideSecrets(cItem: ChatItem): String { private fun hideSecrets(cItem: ChatItem) : String {
val md = cItem.formattedText val md = cItem.formattedText
return if (md != null) { return if (md != null) {
var res = "" var res = ""
@@ -282,8 +272,8 @@ object NtfManager {
* old ones if needed * old ones if needed
* */ * */
fun createNtfChannelsMaybeShowAlert() { fun createNtfChannelsMaybeShowAlert() {
manager.createNotificationChannel(NotificationChannel(MessageChannel, generalGetString(MR.strings.ntf_channel_messages), NotificationManager.IMPORTANCE_HIGH)) manager.createNotificationChannel(NotificationChannel(MessageChannel, generalGetString(R.string.ntf_channel_messages), NotificationManager.IMPORTANCE_HIGH))
manager.createNotificationChannel(callNotificationChannel(CallChannel, generalGetString(MR.strings.ntf_channel_calls))) manager.createNotificationChannel(callNotificationChannel(CallChannel, generalGetString(R.string.ntf_channel_calls)))
// Remove old channels since they can't be edited // Remove old channels since they can't be edited
manager.deleteNotificationChannel("chat.simplex.app.CALL_NOTIFICATION") manager.deleteNotificationChannel("chat.simplex.app.CALL_NOTIFICATION")
manager.deleteNotificationChannel("chat.simplex.app.LOCK_SCREEN_CALL_NOTIFICATION") manager.deleteNotificationChannel("chat.simplex.app.LOCK_SCREEN_CALL_NOTIFICATION")
@@ -308,16 +298,14 @@ object NtfManager {
} }
val apiId = chatId.replace("<@", "").toLongOrNull() ?: return val apiId = chatId.replace("<@", "").toLongOrNull() ?: return
acceptContactRequest(apiId, cInfo, isCurrentUser, m) acceptContactRequest(apiId, cInfo, isCurrentUser, m)
cancelNotificationsForChat(chatId) m.controller.ntfManager.cancelNotificationsForChat(chatId)
} }
RejectCallAction -> { RejectCallAction -> {
val invitation = m.callInvitations[chatId] val invitation = m.callInvitations[chatId]
if (invitation != null) { if (invitation != null) {
m.callManager.endCall(invitation = invitation) m.callManager.endCall(invitation = invitation)
} }
} }
else -> { else -> {
Log.e(TAG, "Unknown action. Make sure you provide action from NotificationAction enum") Log.e(TAG, "Unknown action. Make sure you provide action from NotificationAction enum")
} }
@@ -21,6 +21,7 @@ val ToolbarDark = Color(80, 80, 80, 12)
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)
val IncomingCallDark = Color(34, 30, 29, 255)
val WarningOrange = Color(255, 127, 0, 255) 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)
@@ -13,7 +13,6 @@ import androidx.compose.ui.unit.dp
import chat.simplex.app.R import chat.simplex.app.R
import chat.simplex.app.SimplexApp import chat.simplex.app.SimplexApp
import chat.simplex.app.views.helpers.* import chat.simplex.app.views.helpers.*
import chat.simplex.res.MR
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@@ -70,15 +69,15 @@ enum class ThemeColor {
val text: String val text: String
get() = when (this) { get() = when (this) {
PRIMARY -> generalGetString(MR.strings.color_primary) PRIMARY -> generalGetString(R.string.color_primary)
PRIMARY_VARIANT -> generalGetString(MR.strings.color_primary_variant) PRIMARY_VARIANT -> generalGetString(R.string.color_primary_variant)
SECONDARY -> generalGetString(MR.strings.color_secondary) SECONDARY -> generalGetString(R.string.color_secondary)
SECONDARY_VARIANT -> generalGetString(MR.strings.color_secondary_variant) SECONDARY_VARIANT -> generalGetString(R.string.color_secondary_variant)
BACKGROUND -> generalGetString(MR.strings.color_background) BACKGROUND -> generalGetString(R.string.color_background)
SURFACE -> generalGetString(MR.strings.color_surface) SURFACE -> generalGetString(R.string.color_surface)
TITLE -> generalGetString(MR.strings.color_title) TITLE -> generalGetString(R.string.color_title)
SENT_MESSAGE -> generalGetString(MR.strings.color_sent_message) SENT_MESSAGE -> generalGetString(R.string.color_sent_message)
RECEIVED_MESSAGE -> generalGetString(MR.strings.color_received_message) RECEIVED_MESSAGE -> generalGetString(R.string.color_received_message)
} }
} }
@@ -129,29 +128,11 @@ data class ThemeColors(
receivedMessage = receivedMessage?.colorFromReadableHex() ?: baseColors.receivedMessage, receivedMessage = receivedMessage?.colorFromReadableHex() ?: baseColors.receivedMessage,
) )
} }
fun withFilledColors(base: DefaultTheme): ThemeColors {
val c = toColors(base)
val ac = toAppColors(base)
return ThemeColors(
primary = c.primary.toReadableHex(),
primaryVariant = c.primaryVariant.toReadableHex(),
secondary = c.secondary.toReadableHex(),
secondaryVariant = c.secondaryVariant.toReadableHex(),
background = c.background.toReadableHex(),
surface = c.surface.toReadableHex(),
title = ac.title.toReadableHex(),
sentMessage = ac.sentMessage.toReadableHex(),
receivedMessage = ac.receivedMessage.toReadableHex()
)
}
} }
private fun String.colorFromReadableHex(): Color = private fun String.colorFromReadableHex(): Color =
Color(this.replace("#", "").toLongOrNull(16) ?: Color.White.toArgb().toLong()) Color(this.replace("#", "").toLongOrNull(16) ?: Color.White.toArgb().toLong())
private fun Color.toReadableHex(): String = "#" + Integer.toHexString(toArgb())
@Serializable @Serializable
data class ThemeOverrides ( data class ThemeOverrides (
val base: DefaultTheme, val base: DefaultTheme,
@@ -172,6 +153,9 @@ data class ThemeOverrides (
} }
} }
@Serializable
data class ThemeData (val colors: ThemeColors)
fun Modifier.themedBackground(baseTheme: DefaultTheme = CurrentColors.value.base, shape: Shape = RectangleShape): Modifier { fun Modifier.themedBackground(baseTheme: DefaultTheme = CurrentColors.value.base, shape: Shape = RectangleShape): Modifier {
return if (baseTheme == DefaultTheme.SIMPLEX) { return if (baseTheme == DefaultTheme.SIMPLEX) {
this.background(brush = Brush.linearGradient( this.background(brush = Brush.linearGradient(
@@ -7,7 +7,7 @@ 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 chat.simplex.res.MR import okhttp3.internal.toHexString
object ThemeManager { object ThemeManager {
private val appPrefs: AppPreferences by lazy { private val appPrefs: AppPreferences by lazy {
@@ -44,16 +44,19 @@ object ThemeManager {
return ActiveTheme(themeName, baseTheme.first, theme.colors.toColors(theme.base), theme.colors.toAppColors(theme.base)) return ActiveTheme(themeName, baseTheme.first, theme.colors.toColors(theme.base), theme.colors.toAppColors(theme.base))
} }
fun currentThemeOverridesForExport(darkForSystemTheme: Boolean): ThemeOverrides { fun currentThemeData(darkForSystemTheme: Boolean): ThemeData {
val themeName = appPrefs.currentTheme.get()!! val t = currentColors(darkForSystemTheme)
val nonSystemThemeName = if (themeName != DefaultTheme.SYSTEM.name) { return ThemeData(colors = ThemeColors(
themeName primary = t.colors.primary.toReadableHex(),
} else { primaryVariant = t.colors.primaryVariant.toReadableHex(),
if (darkForSystemTheme) appPrefs.systemDarkTheme.get()!! else DefaultTheme.LIGHT.name secondary = t.colors.secondary.toReadableHex(),
} secondaryVariant = t.colors.secondaryVariant.toReadableHex(),
val overrides = appPrefs.themeOverrides.get().toMutableMap() background = t.colors.background.toReadableHex(),
val nonFilledTheme = overrides[nonSystemThemeName] ?: ThemeOverrides(base = CurrentColors.value.base, colors = ThemeColors()) surface = t.colors.surface.toReadableHex(),
return nonFilledTheme.copy(colors = nonFilledTheme.colors.withFilledColors(CurrentColors.value.base)) 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
@@ -63,28 +66,28 @@ object ThemeManager {
Triple( Triple(
if (darkForSystemTheme) systemDarkThemeColors().first else LightColorPalette, if (darkForSystemTheme) systemDarkThemeColors().first else LightColorPalette,
DefaultTheme.SYSTEM, DefaultTheme.SYSTEM,
generalGetString(MR.strings.theme_system) generalGetString(R.string.theme_system)
) )
) )
allThemes.add( allThemes.add(
Triple( Triple(
LightColorPalette, LightColorPalette,
DefaultTheme.LIGHT, DefaultTheme.LIGHT,
generalGetString(MR.strings.theme_light) generalGetString(R.string.theme_light)
) )
) )
allThemes.add( allThemes.add(
Triple( Triple(
DarkColorPalette, DarkColorPalette,
DefaultTheme.DARK, DefaultTheme.DARK,
generalGetString(MR.strings.theme_dark) generalGetString(R.string.theme_dark)
) )
) )
allThemes.add( allThemes.add(
Triple( Triple(
SimplexColorPalette, SimplexColorPalette,
DefaultTheme.SIMPLEX, DefaultTheme.SIMPLEX,
generalGetString(MR.strings.theme_simplex) generalGetString(R.string.theme_simplex)
) )
) )
return allThemes return allThemes
@@ -125,12 +128,11 @@ object ThemeManager {
CurrentColors.value = currentColors(!CurrentColors.value.colors.isLight) CurrentColors.value = currentColors(!CurrentColors.value.colors.isLight)
} }
fun saveAndApplyThemeOverrides(theme: ThemeOverrides, darkForSystemTheme: Boolean) { fun saveAndApplyThemeData(name: String, theme: ThemeData, darkForSystemTheme: Boolean) {
val overrides = appPrefs.themeOverrides.get().toMutableMap() val overrides = appPrefs.themeOverrides.get().toMutableMap()
val prevValue = overrides[theme.base.name] ?: ThemeOverrides(base = CurrentColors.value.base, colors = ThemeColors()) val prevValue = overrides[name] ?: ThemeOverrides(base = CurrentColors.value.base, colors = ThemeColors())
overrides[theme.base.name] = prevValue.copy(colors = theme.colors) overrides[name] = prevValue.copy(colors = theme.colors)
appPrefs.themeOverrides.set(overrides) appPrefs.themeOverrides.set(overrides)
appPrefs.currentTheme.set(theme.base.name)
CurrentColors.value = currentColors(!CurrentColors.value.colors.isLight) CurrentColors.value = currentColors(!CurrentColors.value.colors.isLight)
} }
@@ -149,4 +151,4 @@ object ThemeManager {
} }
} }
private fun Color.toReadableHex(): String = "#" + Integer.toHexString(toArgb()) private fun Color.toReadableHex(): String = "#" + toArgb().toHexString()
@@ -4,16 +4,16 @@ import androidx.compose.material.Typography
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.* import androidx.compose.ui.text.font.*
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import chat.simplex.res.MR import chat.simplex.app.R
// https://github.com/rsms/inter // https://github.com/rsms/inter
val Inter: FontFamily = FontFamily( val Inter = FontFamily(
Font(MR.fonts.Inter.regular.fontResourceId), Font(R.font.inter_regular),
Font(MR.fonts.Inter.italic.fontResourceId, style = FontStyle.Italic), Font(R.font.inter_italic, style = FontStyle.Italic),
Font(MR.fonts.Inter.bold.fontResourceId, FontWeight.Bold), Font(R.font.inter_bold, weight = FontWeight.Bold),
Font(MR.fonts.Inter.semibold.fontResourceId, FontWeight.SemiBold), Font(R.font.inter_semi_bold, weight = FontWeight.SemiBold),
Font(MR.fonts.Inter.medium.fontResourceId, FontWeight.Medium), Font(R.font.inter_medium, weight = FontWeight.Medium),
Font(MR.fonts.Inter.light.fontResourceId, FontWeight.Light) Font(R.font.inter_light, weight = FontWeight.Light),
) )
// Set of Material typography styles to start with // Set of Material typography styles to start with
@@ -33,11 +33,6 @@ val Typography = Typography(
fontWeight = FontWeight.Normal, fontWeight = FontWeight.Normal,
fontSize = 18.5.sp fontSize = 18.5.sp
), ),
h4 = TextStyle(
fontFamily = Inter,
fontWeight = FontWeight.Normal,
fontSize = 17.5.sp
),
body1 = TextStyle( body1 = TextStyle(
fontFamily = Inter, fontFamily = Inter,
fontWeight = FontWeight.Normal, fontWeight = FontWeight.Normal,
@@ -14,7 +14,7 @@ fun SplashView() {
color = MaterialTheme.colors.background color = MaterialTheme.colors.background
) { ) {
// Image( // Image(
// painter = painterResource(MR.images.logo), // painter = painterResource(R.drawable.logo),
// contentDescription = "Simplex Icon", // contentDescription = "Simplex Icon",
// modifier = Modifier // modifier = Modifier
// .height(230.dp) // .height(230.dp)
@@ -86,7 +86,7 @@ fun TerminalLayout(
userIsObserver = false, userIsObserver = false,
userCanSend = true, userCanSend = true,
allowVoiceToContact = {}, allowVoiceToContact = {},
sendMessage = { sendCommand() }, sendMessage = sendCommand,
sendLiveMessage = null, sendLiveMessage = null,
updateLiveMessage = null, updateLiveMessage = null,
onMessageChange = ::onMessageChange, onMessageChange = ::onMessageChange,
@@ -99,8 +99,8 @@ fun TerminalLayout(
Surface( Surface(
modifier = Modifier modifier = Modifier
.padding(contentPadding) .padding(contentPadding)
.fillMaxWidth(), .fillMaxWidth()
color = MaterialTheme.colors.background .themedBackground()
) { ) {
TerminalLog(terminalItems) TerminalLog(terminalItems)
} }
@@ -128,7 +128,7 @@ fun TerminalLog(terminalItems: List<TerminalItem>) {
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.clickable { .clickable {
ModalManager.shared.showModal(endButtons = { ShareButton { shareText(item.details) } }) { 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))
} }
@@ -14,14 +14,16 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.* 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 dev.icerock.moko.resources.compose.painterResource import androidx.compose.ui.res.painterResource
import dev.icerock.moko.resources.compose.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
import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.style.* 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.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.*
@@ -29,7 +31,6 @@ 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.onboarding.ReadableText import chat.simplex.app.views.onboarding.ReadableText
import com.google.accompanist.insets.navigationBarsWithImePadding import com.google.accompanist.insets.navigationBarsWithImePadding
import chat.simplex.res.MR
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
@@ -43,75 +44,77 @@ fun CreateProfilePanel(chatModel: ChatModel, close: () -> Unit) {
val fullName = rememberSaveable { mutableStateOf("") } val fullName = rememberSaveable { mutableStateOf("") }
val focusRequester = remember { FocusRequester() } val focusRequester = remember { FocusRequester() }
Column( Surface(Modifier.background(MaterialTheme.colors.onBackground)) {
modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()) Column(
) { modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState())
/*CloseSheetBar(close = { ) {
if (chatModel.users.isEmpty()) { /*CloseSheetBar(close = {
chatModel.onboardingStage.value = OnboardingStage.Step1_SimpleXInfo if (chatModel.users.isEmpty()) {
} else { chatModel.onboardingStage.value = OnboardingStage.Step1_SimpleXInfo
close() } else {
} close()
})*/ }
Column(Modifier.padding(horizontal = DEFAULT_PADDING)) { })*/
AppBarTitle(stringResource(MR.strings.create_profile), bottomPadding = DEFAULT_PADDING) Column(Modifier.padding(horizontal = DEFAULT_PADDING * 1f)) {
ReadableText(MR.strings.your_profile_is_stored_on_your_device, TextAlign.Center, padding = PaddingValues(), style = MaterialTheme.typography.body1) AppBarTitle(stringResource(R.string.create_profile))
ReadableText(MR.strings.profile_is_only_shared_with_your_contacts, TextAlign.Center, style = MaterialTheme.typography.body1) ReadableText(R.string.your_profile_is_stored_on_your_device, TextAlign.Center, padding = PaddingValues())
Spacer(Modifier.height(DEFAULT_PADDING)) ReadableText(R.string.profile_is_only_shared_with_your_contacts, TextAlign.Center)
Row(Modifier.padding(bottom = DEFAULT_PADDING_HALF).fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { Spacer(Modifier.height(DEFAULT_PADDING * 1.5f))
Text( Row(Modifier.padding(bottom = DEFAULT_PADDING_HALF).fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
stringResource(MR.strings.display_name),
fontSize = 16.sp
)
if (!isValidDisplayName(displayName.value)) {
Text( Text(
stringResource(MR.strings.no_spaces), stringResource(R.string.display_name),
fontSize = 16.sp, fontSize = 16.sp
color = Color.Red
) )
if (!isValidDisplayName(displayName.value)) {
Text(
stringResource(R.string.no_spaces),
fontSize = 16.sp,
color = Color.Red
)
}
}
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))
Row {
if (chatModel.users.isEmpty()) {
SimpleButtonDecorated(
text = stringResource(R.string.about_simplex),
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))
val enabled = displayName.value.isNotEmpty() && isValidDisplayName(displayName.value)
val createModifier: Modifier
val createColor: Color
if (enabled) {
createModifier = Modifier.clickable { createProfile(chatModel, displayName.value, fullName.value, close) }.padding(8.dp)
createColor = MaterialTheme.colors.primary
} else {
createModifier = Modifier.padding(8.dp)
createColor = MaterialTheme.colors.secondary
}
Surface(shape = RoundedCornerShape(20.dp)) {
Row(verticalAlignment = Alignment.CenterVertically, modifier = createModifier) {
Text(stringResource(R.string.create_profile_button), style = MaterialTheme.typography.caption, color = createColor, fontWeight = FontWeight.Medium)
Icon(painterResource(R.drawable.ic_arrow_forward_ios), stringResource(R.string.create_profile_button), tint = createColor)
}
} }
} }
ProfileNameField(displayName, "", ::isValidDisplayName, focusRequester)
Spacer(Modifier.height(DEFAULT_PADDING))
Text(
stringResource(MR.strings.full_name_optional__prompt),
fontSize = 16.sp,
modifier = Modifier.padding(bottom = DEFAULT_PADDING_HALF)
)
ProfileNameField(fullName, "")
}
Spacer(Modifier.fillMaxHeight().weight(1f))
Row {
if (chatModel.users.isEmpty()) {
SimpleButtonDecorated(
text = stringResource(MR.strings.about_simplex),
icon = painterResource(MR.images.ic_arrow_back_ios_new),
textDecoration = TextDecoration.None,
fontWeight = FontWeight.Medium
) { chatModel.onboardingStage.value = OnboardingStage.Step1_SimpleXInfo }
}
Spacer(Modifier.fillMaxWidth().weight(1f))
val enabled = displayName.value.isNotEmpty() && isValidDisplayName(displayName.value)
val createModifier: Modifier
val createColor: Color
if (enabled) {
createModifier = Modifier.clickable { createProfile(chatModel, displayName.value, fullName.value, close) }.padding(8.dp)
createColor = MaterialTheme.colors.primary
} else {
createModifier = Modifier.padding(8.dp)
createColor = MaterialTheme.colors.secondary
}
Surface(shape = RoundedCornerShape(20.dp), color = Color.Transparent) {
Row(verticalAlignment = Alignment.CenterVertically, modifier = createModifier) {
Text(stringResource(MR.strings.create_profile_button), style = MaterialTheme.typography.caption, color = createColor, fontWeight = FontWeight.Medium)
Icon(painterResource(MR.images.ic_arrow_forward_ios), stringResource(MR.strings.create_profile_button), tint = createColor)
}
}
}
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
delay(300) delay(300)
focusRequester.requestFocus() focusRequester.requestFocus()
}
} }
} }
} }
@@ -124,17 +127,13 @@ fun createProfile(chatModel: ChatModel, displayName: String, fullName: String, c
chatModel.currentUser.value = user chatModel.currentUser.value = user
if (chatModel.users.isEmpty()) { if (chatModel.users.isEmpty()) {
chatModel.controller.startChat(user) chatModel.controller.startChat(user)
chatModel.controller.appPrefs.onboardingStage.set(OnboardingStage.Step3_CreateSimpleXAddress) chatModel.onboardingStage.value = OnboardingStage.Step3_SetNotificationsMode
chatModel.onboardingStage.value = OnboardingStage.Step3_CreateSimpleXAddress SimplexApp.context.chatModel.controller.ntfManager.createNtfChannelsMaybeShowAlert()
} else { } else {
val users = chatModel.controller.listUsers() val users = chatModel.controller.listUsers()
chatModel.users.clear() chatModel.users.clear()
chatModel.users.addAll(users) chatModel.users.addAll(users)
chatModel.controller.getUserChatData() chatModel.controller.getUserChatData()
// the next two lines are only needed for failure case when because of the database error the app gets stuck on on-boarding screen,
// this will get it unstuck.
chatModel.controller.appPrefs.onboardingStage.set(OnboardingStage.OnboardingComplete)
chatModel.onboardingStage.value = OnboardingStage.OnboardingComplete
close() close()
} }
} }
@@ -26,6 +26,7 @@ class CallManager(val chatModel: ChatModel) {
} }
fun acceptIncomingCall(invitation: RcvCallInvitation) { fun acceptIncomingCall(invitation: RcvCallInvitation) {
ModalManager.shared.closeModals()
val call = chatModel.activeCall.value val call = chatModel.activeCall.value
if (call == null) { if (call == null) {
justAcceptIncomingCall(invitation = invitation) justAcceptIncomingCall(invitation = invitation)
@@ -13,6 +13,7 @@ import android.util.Log
import android.view.ViewGroup import android.view.ViewGroup
import android.webkit.* import android.webkit.*
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
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.runtime.* import androidx.compose.runtime.*
@@ -23,8 +24,8 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.painter.Painter 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 dev.icerock.moko.resources.compose.painterResource import androidx.compose.ui.res.painterResource
import dev.icerock.moko.resources.compose.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
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@@ -41,8 +42,6 @@ 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
import com.google.accompanist.permissions.rememberMultiplePermissionsState import com.google.accompanist.permissions.rememberMultiplePermissionsState
import chat.simplex.res.MR
import dev.icerock.moko.resources.StringResource
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.serialization.decodeFromString import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
@@ -59,7 +58,7 @@ fun ActiveCallView(chatModel: ChatModel) {
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.
// It's needed to prevent Android from shutting down a microphone after a minute or so when screen is off // It's needed to prevent Android from shutting down a microphone after a minute or so when screen is off
if (!ntfModeService) SimplexService.start() if (!ntfModeService) SimplexService.start(SimplexApp.context)
} }
DisposableEffect(Unit) { DisposableEffect(Unit) {
val am = SimplexApp.context.getSystemService(Context.AUDIO_SERVICE) as AudioManager val am = SimplexApp.context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
@@ -186,12 +185,10 @@ fun ActiveCallView(chatModel: ChatModel) {
activity.volumeControlStream = AudioManager.STREAM_VOICE_CALL 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
chatModel.activeCallViewIsVisible.value = true
onDispose { onDispose {
activity.volumeControlStream = prevVolumeControlStream activity.volumeControlStream = prevVolumeControlStream
// Unlock orientation // Unlock orientation
activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
chatModel.activeCallViewIsVisible.value = false
} }
} }
} }
@@ -273,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(painterResource(MR.images.ic_call_end_filled), stringResource(MR.strings.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, painterResource(MR.images.ic_flip_camera_android_filled), MR.strings.icon_descr_flip_camera, flipCamera) ControlButton(call, painterResource(R.drawable.ic_flip_camera_android_filled), R.string.icon_descr_flip_camera, flipCamera)
ControlButton(call, painterResource(MR.images.ic_videocam_filled), MR.strings.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, painterResource(MR.images.ic_videocam_off), MR.strings.icon_descr_video_on, toggleVideo) ControlButton(call, painterResource(R.drawable.ic_videocam_off), R.string.icon_descr_video_on, toggleVideo)
} }
} }
} }
@@ -298,7 +295,7 @@ private fun ActiveCallOverlayLayout(
Box(Modifier.fillMaxWidth().padding(bottom = DEFAULT_BOTTOM_PADDING), 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(painterResource(MR.images.ic_call_end_filled), stringResource(MR.strings.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)) {
@@ -316,7 +313,7 @@ private fun ActiveCallOverlayLayout(
} }
@Composable @Composable
private fun ControlButton(call: Call, icon: Painter, iconText: StringResource, action: () -> Unit, enabled: Boolean = true) { private fun ControlButton(call: Call, icon: Painter, @StringRes iconText: Int, action: () -> Unit, enabled: Boolean = true) {
if (call.hasMedia) { if (call.hasMedia) {
IconButton(onClick = action, enabled = enabled) { IconButton(onClick = action, enabled = enabled) {
Icon(icon, stringResource(iconText), tint = if (enabled) Color(0xFFFFFFD8) else MaterialTheme.colors.secondary, modifier = Modifier.size(40.dp)) Icon(icon, stringResource(iconText), tint = if (enabled) Color(0xFFFFFFD8) else MaterialTheme.colors.secondary, modifier = Modifier.size(40.dp))
@@ -329,18 +326,18 @@ private fun ControlButton(call: Call, icon: Painter, iconText: StringResource, a
@Composable @Composable
private fun ToggleAudioButton(call: Call, toggleAudio: () -> Unit) { private fun ToggleAudioButton(call: Call, toggleAudio: () -> Unit) {
if (call.audioEnabled) { if (call.audioEnabled) {
ControlButton(call, painterResource(MR.images.ic_mic), MR.strings.icon_descr_audio_off, toggleAudio) ControlButton(call, painterResource(R.drawable.ic_mic), R.string.icon_descr_audio_off, toggleAudio)
} else { } else {
ControlButton(call, painterResource(MR.images.ic_mic_off), MR.strings.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, enabled: Boolean, toggleSound: () -> Unit) { private fun ToggleSoundButton(call: Call, enabled: Boolean, toggleSound: () -> Unit) {
if (call.soundSpeaker) { if (call.soundSpeaker) {
ControlButton(call, painterResource(MR.images.ic_volume_up), MR.strings.icon_descr_speaker_off, toggleSound, enabled) ControlButton(call, painterResource(R.drawable.ic_volume_up), R.string.icon_descr_speaker_off, toggleSound, enabled)
} else { } else {
ControlButton(call, painterResource(MR.images.ic_volume_down), MR.strings.icon_descr_speaker_on, toggleSound, enabled) ControlButton(call, painterResource(R.drawable.ic_volume_down), R.string.icon_descr_speaker_on, toggleSound, enabled)
} }
} }
@@ -24,8 +24,8 @@ import androidx.compose.ui.draw.scale
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import dev.icerock.moko.resources.compose.painterResource import androidx.compose.ui.res.painterResource
import dev.icerock.moko.resources.compose.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
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@@ -33,10 +33,9 @@ import androidx.compose.ui.unit.sp
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.model.NtfManager.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.res.MR
import kotlinx.datetime.Clock import kotlinx.datetime.Clock
class IncomingCallActivity: ComponentActivity() { class IncomingCallActivity: ComponentActivity() {
@@ -98,9 +97,8 @@ fun IncomingCallActivityView(m: ChatModel) {
SimpleXTheme { SimpleXTheme {
Surface( Surface(
Modifier Modifier
.fillMaxSize(), .themedBackground()
color = MaterialTheme.colors.background .fillMaxSize()) {
) {
if (showCallView) { if (showCallView) {
Box { Box {
ActiveCallView(m) ActiveCallView(m)
@@ -171,18 +169,18 @@ fun IncomingCallLockScreenAlertLayout(
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(MR.strings.reject), painterResource(MR.images.ic_call_end_filled), 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(MR.strings.ignore), painterResource(MR.images.ic_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(MR.strings.accept), painterResource(MR.images.ic_check_filled), 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(MR.strings.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(MR.strings.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(MR.strings.open_verb), icon = painterResource(MR.images.ic_check_filled), click = openApp) SimpleButton(text = stringResource(R.string.open_verb), icon = painterResource(R.drawable.ic_check_filled), click = openApp)
} }
} }
} }
@@ -190,8 +188,8 @@ fun IncomingCallLockScreenAlertLayout(
@Composable @Composable
private fun SimpleXLogo() { private fun SimpleXLogo() {
Image( Image(
painter = painterResource(if (isInDarkTheme()) MR.images.logo_light else MR.images.logo), painter = painterResource(if (isInDarkTheme()) R.drawable.logo_light else R.drawable.logo),
contentDescription = stringResource(MR.strings.image_descr_simplex_logo), contentDescription = stringResource(R.string.image_descr_simplex_logo),
modifier = Modifier modifier = Modifier
.padding(vertical = DEFAULT_PADDING) .padding(vertical = DEFAULT_PADDING)
.fillMaxWidth(0.80f) .fillMaxWidth(0.80f)
@@ -228,9 +226,8 @@ fun PreviewIncomingCallLockScreenAlert() {
SimpleXTheme(true) { SimpleXTheme(true) {
Surface( Surface(
Modifier Modifier
.fillMaxSize(), .themedBackground()
color = MaterialTheme.colors.background .fillMaxSize()) {
) {
IncomingCallLockScreenAlertLayout( IncomingCallLockScreenAlertLayout(
invitation = RcvCallInvitation( invitation = RcvCallInvitation(
user = User.sampleData, user = User.sampleData,
@@ -11,8 +11,8 @@ import androidx.compose.ui.draw.scale
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import dev.icerock.moko.resources.compose.painterResource import androidx.compose.ui.res.painterResource
import dev.icerock.moko.resources.compose.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
@@ -21,7 +21,6 @@ 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
import chat.simplex.app.views.usersettings.ProfilePreview import chat.simplex.app.views.usersettings.ProfilePreview
import chat.simplex.res.MR
import kotlinx.datetime.Clock import kotlinx.datetime.Clock
@Composable @Composable
@@ -29,7 +28,7 @@ fun IncomingCallAlertView(invitation: RcvCallInvitation, chatModel: ChatModel) {
val cm = chatModel.callManager val cm = chatModel.callManager
val cxt = LocalContext.current val cxt = LocalContext.current
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
LaunchedEffect(true) { SoundPlayer.shared.start(scope, sound = !chatModel.showCallView.value) } LaunchedEffect(true) { SoundPlayer.shared.start(cxt, scope, sound = !chatModel.showCallView.value) }
DisposableEffect(true) { onDispose { SoundPlayer.shared.stop() } } DisposableEffect(true) { onDispose { SoundPlayer.shared.stop() } }
IncomingCallAlertLayout( IncomingCallAlertLayout(
invitation, invitation,
@@ -51,18 +50,18 @@ fun IncomingCallAlertLayout(
ignoreCall: () -> Unit, ignoreCall: () -> Unit,
acceptCall: () -> Unit acceptCall: () -> Unit
) { ) {
val color = if (isInDarkTheme()) MaterialTheme.colors.surface else IncomingCallLight val color = if (isInDarkTheme()) IncomingCallDark else IncomingCallLight
Column(Modifier.fillMaxWidth().background(color).padding(top = DEFAULT_PADDING, bottom = DEFAULT_PADDING, start = DEFAULT_PADDING, end = 8.dp)) { Column(Modifier.fillMaxWidth().background(color).padding(top = DEFAULT_PADDING, bottom = DEFAULT_PADDING, start = DEFAULT_PADDING, end = 8.dp)) {
IncomingCallInfo(invitation, chatModel) 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) ProfilePreview(profileOf = invitation.contact, size = 64.dp, color = Color.White)
} }
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
CallButton(stringResource(MR.strings.reject), painterResource(MR.images.ic_call_end_filled), Color.Red, rejectCall) CallButton(stringResource(R.string.reject), painterResource(R.drawable.ic_call_end_filled), Color.Red, rejectCall)
CallButton(stringResource(MR.strings.ignore), painterResource(MR.images.ic_close), MaterialTheme.colors.primary, ignoreCall) CallButton(stringResource(R.string.ignore), painterResource(R.drawable.ic_close), MaterialTheme.colors.primary, ignoreCall)
CallButton(stringResource(MR.strings.accept), painterResource(MR.images.ic_check_filled), SimplexGreen, acceptCall) CallButton(stringResource(R.string.accept), painterResource(R.drawable.ic_check_filled), SimplexGreen, acceptCall)
} }
} }
} }
@@ -76,10 +75,10 @@ fun IncomingCallInfo(invitation: RcvCallInvitation, chatModel: ChatModel) {
ProfileImage(size = 32.dp, image = invitation.user.profile.image, color = MaterialTheme.colors.secondaryVariant) ProfileImage(size = 32.dp, image = invitation.user.profile.image, color = MaterialTheme.colors.secondaryVariant)
Spacer(Modifier.width(4.dp)) Spacer(Modifier.width(4.dp))
} }
if (invitation.callType.media == CallMediaType.Video) CallIcon(painterResource(MR.images.ic_videocam_filled), stringResource(MR.strings.icon_descr_video_call)) if (invitation.callType.media == CallMediaType.Video) CallIcon(painterResource(R.drawable.ic_videocam_filled), stringResource(R.string.icon_descr_video_call))
else CallIcon(painterResource(MR.images.ic_call_filled), stringResource(MR.strings.icon_descr_audio_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, color = MaterialTheme.colors.onBackground) Text(invitation.callTypeText)
} }
} }
@@ -16,7 +16,7 @@ class SoundPlayer {
private var player: MediaPlayer? = null private var player: MediaPlayer? = null
var playing = false var playing = false
fun start(scope: CoroutineScope, sound: Boolean) { fun start(cxt: Context, scope: CoroutineScope, sound: Boolean) {
player?.reset() player?.reset()
player = MediaPlayer().apply { player = MediaPlayer().apply {
setAudioAttributes( setAudioAttributes(
@@ -28,7 +28,7 @@ class SoundPlayer {
setDataSource(SimplexApp.context, Uri.parse("android.resource://" + SimplexApp.context.packageName + "/" + R.raw.ring_once)) setDataSource(SimplexApp.context, Uri.parse("android.resource://" + SimplexApp.context.packageName + "/" + R.raw.ring_once))
prepare() prepare()
} }
val vibrator = ContextCompat.getSystemService(SimplexApp.context, Vibrator::class.java) val vibrator = ContextCompat.getSystemService(cxt, Vibrator::class.java)
val effect = VibrationEffect.createOneShot(250, VibrationEffect.DEFAULT_AMPLITUDE) val effect = VibrationEffect.createOneShot(250, VibrationEffect.DEFAULT_AMPLITUDE)
playing = true playing = true
withScope(scope) { withScope(scope) {
@@ -1,12 +1,13 @@
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 dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.toUpperCase
import chat.simplex.app.* 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.model.User
import chat.simplex.app.views.helpers.generalGetString import chat.simplex.app.views.helpers.generalGetString
import chat.simplex.res.MR
import kotlinx.datetime.Instant import kotlinx.datetime.Instant
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@@ -32,9 +33,9 @@ data class Call(
val encryptionStatus: String @Composable get() = when(callState) { val encryptionStatus: String @Composable get() = when(callState) {
CallState.WaitCapabilities -> "" CallState.WaitCapabilities -> ""
CallState.InvitationSent -> stringResource(if (localEncrypted) MR.strings.status_e2e_encrypted else MR.strings.status_no_e2e_encryption) CallState.InvitationSent -> stringResource(if (localEncrypted) R.string.status_e2e_encrypted else R.string.status_no_e2e_encryption)
CallState.InvitationAccepted -> stringResource(if (sharedKey == null) MR.strings.status_contact_has_no_e2e_encryption else MR.strings.status_contact_has_e2e_encryption) CallState.InvitationAccepted -> stringResource(if (sharedKey == null) R.string.status_contact_has_no_e2e_encryption else R.string.status_contact_has_e2e_encryption)
else -> stringResource(if (!localEncrypted) MR.strings.status_no_e2e_encryption else if (sharedKey == null) MR.strings.status_contact_has_no_e2e_encryption else MR.strings.status_e2e_encrypted) else -> stringResource(if (!localEncrypted) R.string.status_no_e2e_encryption else if (sharedKey == null) R.string.status_contact_has_no_e2e_encryption else R.string.status_e2e_encrypted)
} }
val hasMedia: Boolean get() = callState == CallState.OfferSent || callState == CallState.Negotiated || callState == CallState.Connected val hasMedia: Boolean get() = callState == CallState.OfferSent || callState == CallState.Negotiated || callState == CallState.Connected
@@ -52,15 +53,15 @@ enum class CallState {
Ended; Ended;
val text: String @Composable get() = when(this) { val text: String @Composable get() = when(this) {
WaitCapabilities -> stringResource(MR.strings.callstate_starting) WaitCapabilities -> stringResource(R.string.callstate_starting)
InvitationSent -> stringResource(MR.strings.callstate_waiting_for_answer) InvitationSent -> stringResource(R.string.callstate_waiting_for_answer)
InvitationAccepted -> stringResource(MR.strings.callstate_starting) InvitationAccepted -> stringResource(R.string.callstate_starting)
OfferSent -> stringResource(MR.strings.callstate_waiting_for_confirmation) OfferSent -> stringResource(R.string.callstate_waiting_for_confirmation)
OfferReceived -> stringResource(MR.strings.callstate_received_answer) OfferReceived -> stringResource(R.string.callstate_received_answer)
AnswerReceived -> stringResource(MR.strings.callstate_received_confirmation) AnswerReceived -> stringResource(R.string.callstate_received_confirmation)
Negotiated -> stringResource(MR.strings.callstate_connecting) Negotiated -> stringResource(R.string.callstate_connecting)
Connected -> stringResource(MR.strings.callstate_connected) Connected -> stringResource(R.string.callstate_connected)
Ended -> stringResource(MR.strings.callstate_ended) Ended -> stringResource(R.string.callstate_ended)
} }
} }
@@ -98,12 +99,12 @@ sealed class WCallResponse {
@Serializable data class CallType(val media: CallMediaType, val capabilities: CallCapabilities) @Serializable data class CallType(val media: CallMediaType, val capabilities: CallCapabilities)
@Serializable data class RcvCallInvitation(val user: User, 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) MR.strings.video_call_no_encryption else MR.strings.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) MR.strings.audio_call_no_encryption else MR.strings.encrypted_audio_call CallMediaType.Audio -> if (sharedKey == null) R.string.audio_call_no_encryption else R.string.encrypted_audio_call
}) })
val callTitle: String get() = generalGetString(when(callType.media) { val callTitle: String get() = generalGetString(when(callType.media) {
CallMediaType.Video -> MR.strings.incoming_video_call CallMediaType.Video -> R.string.incoming_video_call
CallMediaType.Audio -> MR.strings.incoming_audio_call CallMediaType.Audio -> R.string.incoming_audio_call
}) })
} }
@Serializable data class CallCapabilities(val encryption: Boolean) @Serializable data class CallCapabilities(val encryption: Boolean)
@@ -113,9 +114,9 @@ sealed class WCallResponse {
val remote = remoteCandidate?.candidateType val remote = remoteCandidate?.candidateType
return when { return when {
local == RTCIceCandidateType.Host && remote == RTCIceCandidateType.Host -> local == RTCIceCandidateType.Host && remote == RTCIceCandidateType.Host ->
stringResource(MR.strings.call_connection_peer_to_peer) stringResource(R.string.call_connection_peer_to_peer)
local == RTCIceCandidateType.Relay && remote == RTCIceCandidateType.Relay -> local == RTCIceCandidateType.Relay && remote == RTCIceCandidateType.Relay ->
stringResource(MR.strings.call_connection_via_relay) stringResource(R.string.call_connection_via_relay)
else -> else ->
"${local?.value ?: "unknown"} / ${remote?.value ?: "unknown"}" "${local?.value ?: "unknown"} / ${remote?.value ?: "unknown"}"
} }
@@ -0,0 +1,465 @@
package chat.simplex.app.views.chat
import InfoRow
import InfoRowEllipsis
import SectionBottomSpacer
import SectionDividerSpaced
import SectionItemView
import SectionSpacer
import SectionView
import android.widget.Toast
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.ClipboardManager
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import chat.simplex.app.R
import chat.simplex.app.SimplexApp
import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.usersettings.*
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.*
import kotlinx.datetime.Clock
@Composable
fun ChatInfoView(
chatModel: ChatModel,
contact: Contact,
connStats: ConnectionStats?,
customUserProfile: Profile?,
localAlias: String,
connectionCode: String?,
close: () -> Unit,
) {
BackHandler(onBack = close)
val chat = chatModel.chats.firstOrNull { it.id == chatModel.chatId.value }
val developerTools = chatModel.controller.appPrefs.developerTools.get()
if (chat != null) {
val contactNetworkStatus = remember(chatModel.networkStatuses.toMap()) {
mutableStateOf(chatModel.contactNetworkStatus(contact))
}
ChatInfoLayout(
chat,
contact,
connStats,
contactNetworkStatus.value,
customUserProfile,
localAlias,
connectionCode,
developerTools,
onLocalAliasChanged = {
setContactAlias(chat.chatInfo.apiId, it, chatModel)
},
openPreferences = {
ModalManager.shared.showCustomModal { close ->
val user = chatModel.currentUser.value
if (user != null) {
ContactPreferencesView(chatModel, user, contact.contactId, close)
}
}
},
deleteContact = { deleteContactDialog(chat.chatInfo, chatModel, close) },
clearChat = { clearChatDialog(chat.chatInfo, chatModel, close) },
switchContactAddress = {
showSwitchContactAddressAlert(chatModel, contact.contactId)
},
verifyClicked = {
ModalManager.shared.showModalCloseable { close ->
remember { derivedStateOf { (chatModel.getContactChat(contact.contactId)?.chatInfo as? ChatInfo.Direct)?.contact } }.value?.let { ct ->
VerifyCodeView(
ct.displayName,
connectionCode,
ct.verified,
verify = { code ->
chatModel.controller.apiVerifyContact(ct.contactId, code)?.let { r ->
val (verified, existingCode) = r
chatModel.updateContact(
ct.copy(
activeConn = ct.activeConn.copy(
connectionCode = if (verified) SecurityCode(existingCode, Clock.System.now()) else null
)
)
)
r
}
},
close,
)
}
}
}
)
}
}
fun deleteContactDialog(chatInfo: ChatInfo, chatModel: ChatModel, close: (() -> Unit)? = null) {
AlertManager.shared.showAlertDialog(
title = generalGetString(R.string.delete_contact_question),
text = generalGetString(R.string.delete_contact_all_messages_deleted_cannot_undo_warning),
confirmText = generalGetString(R.string.delete_verb),
onConfirm = {
withApi {
val r = chatModel.controller.apiDeleteChat(chatInfo.chatType, chatInfo.apiId)
if (r) {
chatModel.removeChat(chatInfo.id)
chatModel.chatId.value = null
chatModel.controller.ntfManager.cancelNotificationsForChat(chatInfo.id)
close?.invoke()
}
}
},
destructive = true,
)
}
fun clearChatDialog(chatInfo: ChatInfo, chatModel: ChatModel, close: (() -> Unit)? = null) {
AlertManager.shared.showAlertDialog(
title = generalGetString(R.string.clear_chat_question),
text = generalGetString(R.string.clear_chat_warning),
confirmText = generalGetString(R.string.clear_verb),
onConfirm = {
withApi {
val updatedChatInfo = chatModel.controller.apiClearChat(chatInfo.chatType, chatInfo.apiId)
if (updatedChatInfo != null) {
chatModel.clearChat(updatedChatInfo)
chatModel.controller.ntfManager.cancelNotificationsForChat(chatInfo.id)
close?.invoke()
}
}
},
destructive = true,
)
}
@Composable
fun ChatInfoLayout(
chat: Chat,
contact: Contact,
connStats: ConnectionStats?,
contactNetworkStatus: NetworkStatus,
customUserProfile: Profile?,
localAlias: String,
connectionCode: String?,
developerTools: Boolean,
onLocalAliasChanged: (String) -> Unit,
openPreferences: () -> Unit,
deleteContact: () -> Unit,
clearChat: () -> Unit,
switchContactAddress: () -> Unit,
verifyClicked: () -> Unit,
) {
Column(
Modifier
.fillMaxWidth()
.verticalScroll(rememberScrollState())
) {
Row(
Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center
) {
ChatInfoHeader(chat.chatInfo, contact)
}
LocalAliasEditor(localAlias, updateValue = onLocalAliasChanged)
SectionSpacer()
if (customUserProfile != null) {
SectionView(generalGetString(R.string.incognito).uppercase()) {
InfoRow(generalGetString(R.string.incognito_random_profile), customUserProfile.chatViewName)
}
SectionDividerSpaced()
}
SectionView {
if (connectionCode != null) {
VerifyCodeButton(contact.verified, verifyClicked)
}
ContactPreferencesButton(openPreferences)
}
SectionDividerSpaced()
SectionView(title = stringResource(R.string.conn_stats_section_title_servers)) {
SwitchAddressButton(switchContactAddress)
if (connStats != null) {
SectionItemView({
AlertManager.shared.showAlertMsg(
generalGetString(R.string.network_status),
contactNetworkStatus.statusExplanation
)}) {
NetworkStatusRow(contactNetworkStatus)
}
val rcvServers = connStats.rcvServers
if (rcvServers != null && rcvServers.isNotEmpty()) {
SimplexServers(stringResource(R.string.receiving_via), rcvServers)
}
val sndServers = connStats.sndServers
if (sndServers != null && sndServers.isNotEmpty()) {
SimplexServers(stringResource(R.string.sending_via), sndServers)
}
}
}
SectionDividerSpaced()
SectionView {
ClearChatButton(clearChat)
DeleteContactButton(deleteContact)
}
if (developerTools) {
SectionDividerSpaced()
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_database_id), chat.chatInfo.apiId.toString())
}
}
SectionBottomSpacer()
}
}
@Composable
fun ChatInfoHeader(cInfo: ChatInfo, contact: Contact) {
Column(
Modifier.padding(horizontal = 8.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
ChatInfoImage(cInfo, size = 192.dp, iconColor = if (isInDarkTheme()) GroupDark else SettingsSecondaryLight)
Row(Modifier.padding(bottom = 8.dp), verticalAlignment = Alignment.CenterVertically) {
if (contact.verified) {
Icon(painterResource(R.drawable.ic_verified_user), null, Modifier.padding(end = 6.dp, top = 4.dp).size(24.dp), tint = MaterialTheme.colors.secondary)
}
Text(
contact.profile.displayName, style = MaterialTheme.typography.h1.copy(fontWeight = FontWeight.Normal),
color = MaterialTheme.colors.onBackground,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
if (cInfo.fullName != "" && cInfo.fullName != cInfo.displayName && cInfo.fullName != contact.profile.displayName) {
Text(
cInfo.fullName, style = MaterialTheme.typography.h2,
color = MaterialTheme.colors.onBackground,
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
}
}
}
@Composable
fun LocalAliasEditor(
initialValue: String,
center: Boolean = true,
leadingIcon: Boolean = false,
focus: Boolean = false,
updateValue: (String) -> Unit
) {
var value by rememberSaveable { mutableStateOf(initialValue) }
val modifier = if (center)
Modifier.padding(horizontal = if (!leadingIcon) DEFAULT_PADDING else 0.dp).widthIn(min = 100.dp)
else
Modifier.padding(horizontal = if (!leadingIcon) DEFAULT_PADDING else 0.dp).fillMaxWidth()
Row(Modifier.fillMaxWidth(), horizontalArrangement = if (center) Arrangement.Center else Arrangement.Start) {
DefaultBasicTextField(
modifier,
value,
{
Text(
generalGetString(R.string.text_field_set_contact_placeholder),
textAlign = if (center) TextAlign.Center else TextAlign.Start,
color = MaterialTheme.colors.secondary
)
},
leadingIcon = if (leadingIcon) {
{ Icon(painterResource(R.drawable.ic_edit_filled), null, Modifier.padding(start = 7.dp)) }
} else null,
color = MaterialTheme.colors.secondary,
focus = focus,
textStyle = TextStyle.Default.copy(textAlign = if (value.isEmpty() || !center) TextAlign.Start else TextAlign.Center),
keyboardActions = KeyboardActions(onDone = { updateValue(value) })
) {
value = it
}
}
LaunchedEffect(Unit) {
snapshotFlow { value }
.onEach { delay(500) } // wait a little after every new character, don't emit until user stops typing
.conflate() // get the latest value
.filter { it == value } // don't process old ones
.collect {
updateValue(value)
}
}
DisposableEffect(Unit) {
onDispose { updateValue(value) } // just in case snapshotFlow will be canceled when user presses Back too fast
}
}
@Composable
private fun NetworkStatusRow(networkStatus: NetworkStatus) {
Row(
Modifier.fillMaxSize(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp)
) {
Text(stringResource(R.string.network_status))
Icon(
painterResource(R.drawable.ic_info),
stringResource(R.string.network_status),
tint = MaterialTheme.colors.primary
)
}
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp)
) {
Text(
networkStatus.statusString,
color = MaterialTheme.colors.secondary
)
ServerImage(networkStatus)
}
}
}
@Composable
private fun ServerImage(networkStatus: NetworkStatus) {
Box(Modifier.size(18.dp)) {
when (networkStatus) {
is NetworkStatus.Connected ->
Icon(painterResource(R.drawable.ic_circle_filled), stringResource(R.string.icon_descr_server_status_connected), tint = MaterialTheme.colors.primaryVariant)
is NetworkStatus.Disconnected ->
Icon(painterResource(R.drawable.ic_pending_filled), stringResource(R.string.icon_descr_server_status_disconnected), tint = MaterialTheme.colors.secondary)
is NetworkStatus.Error ->
Icon(painterResource(R.drawable.ic_error_filled), stringResource(R.string.icon_descr_server_status_error), tint = MaterialTheme.colors.secondary)
else -> Icon(painterResource(R.drawable.ic_circle), stringResource(R.string.icon_descr_server_status_pending), tint = MaterialTheme.colors.secondary)
}
}
}
@Composable
fun SimplexServers(text: String, servers: List<String>) {
val info = servers.joinToString(separator = ", ") { it.substringAfter("@") }
val clipboardManager: ClipboardManager = LocalClipboardManager.current
InfoRowEllipsis(text, info) {
clipboardManager.setText(AnnotatedString(servers.joinToString(separator = ",")))
Toast.makeText(SimplexApp.context, generalGetString(R.string.copied), Toast.LENGTH_SHORT).show()
}
}
@Composable
fun SwitchAddressButton(onClick: () -> Unit) {
SectionItemView(onClick) {
Text(stringResource(R.string.switch_receiving_address), color = MaterialTheme.colors.primary)
}
}
@Composable
fun VerifyCodeButton(contactVerified: Boolean, onClick: () -> Unit) {
SettingsActionItem(
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),
click = onClick,
iconColor = MaterialTheme.colors.secondary,
)
}
@Composable
private fun ContactPreferencesButton(onClick: () -> Unit) {
SettingsActionItem(
painterResource(R.drawable.ic_toggle_on),
stringResource(R.string.contact_preferences),
click = onClick
)
}
@Composable
fun ClearChatButton(onClick: () -> Unit) {
SettingsActionItem(
painterResource(R.drawable.ic_settings_backup_restore),
stringResource(R.string.clear_chat_button),
click = onClick,
textColor = WarningOrange,
iconColor = WarningOrange,
)
}
@Composable
private fun DeleteContactButton(onClick: () -> Unit) {
SettingsActionItem(
painterResource(R.drawable.ic_delete),
stringResource(R.string.button_delete_contact),
click = onClick,
textColor = Color.Red,
iconColor = Color.Red,
)
}
private fun setContactAlias(contactApiId: Long, localAlias: String, chatModel: ChatModel) = withApi {
chatModel.controller.apiSetContactAlias(contactApiId, localAlias)?.let {
chatModel.updateContact(it)
}
}
private fun showSwitchContactAddressAlert(m: ChatModel, contactId: Long) {
AlertManager.shared.showAlertDialog(
title = generalGetString(R.string.switch_receiving_address_question),
text = generalGetString(R.string.switch_receiving_address_desc),
confirmText = generalGetString(R.string.switch_verb),
onConfirm = {
switchContactAddress(m, contactId)
},
destructive = true,
)
}
private fun switchContactAddress(m: ChatModel, contactId: Long) = withApi {
m.controller.apiSwitchContact(contactId)
}
@Preview
@Composable
fun PreviewChatInfoLayout() {
SimpleXTheme {
ChatInfoLayout(
chat = Chat(
chatInfo = ChatInfo.Direct.sampleData,
chatItems = arrayListOf()
),
Contact.sampleData,
localAlias = "",
connectionCode = "123",
developerTools = false,
connStats = null,
contactNetworkStatus = NetworkStatus.Connected(),
onLocalAliasChanged = {},
customUserProfile = null,
openPreferences = {},
deleteContact = {},
clearChat = {},
switchContactAddress = {},
verifyClicked = {},
)
}
}
@@ -20,8 +20,8 @@ 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 dev.icerock.moko.resources.compose.painterResource import androidx.compose.ui.res.painterResource
import dev.icerock.moko.resources.compose.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
@@ -30,6 +30,7 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.* import androidx.compose.ui.unit.*
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.model.* import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.* import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.call.* import chat.simplex.app.views.call.*
@@ -40,7 +41,6 @@ import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.helpers.AppBarHeight import chat.simplex.app.views.helpers.AppBarHeight
import com.google.accompanist.insets.ProvideWindowInsets import com.google.accompanist.insets.ProvideWindowInsets
import com.google.accompanist.insets.navigationBarsWithImePadding import com.google.accompanist.insets.navigationBarsWithImePadding
import chat.simplex.res.MR
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import kotlinx.datetime.Clock import kotlinx.datetime.Clock
@@ -101,7 +101,6 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: () -> Unit) {
} }
} }
val view = LocalView.current val view = LocalView.current
val context = LocalContext.current
if (activeChat.value == null || user == null) { if (activeChat.value == null || user == null) {
chatModel.chatId.value = null chatModel.chatId.value = null
} else { } else {
@@ -166,8 +165,7 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: () -> Unit) {
showMemberInfo = { groupInfo: GroupInfo, member: GroupMember -> showMemberInfo = { groupInfo: GroupInfo, member: GroupMember ->
hideKeyboard(view) hideKeyboard(view)
withApi { withApi {
val r = chatModel.controller.apiGroupMemberInfo(groupInfo.groupId, member.groupMemberId) val stats = chatModel.controller.apiGroupMemberInfo(groupInfo.groupId, member.groupMemberId)
val stats = r?.second
val (_, code) = if (member.memberActive) { val (_, code) = if (member.memberActive) {
try { try {
chatModel.controller.apiGetGroupMemberCode(groupInfo.apiId, member.groupMemberId) chatModel.controller.apiGetGroupMemberCode(groupInfo.apiId, member.groupMemberId)
@@ -260,73 +258,6 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: () -> Unit) {
chatModel.controller.allowFeatureToContact(contact, feature, param) chatModel.controller.allowFeatureToContact(contact, feature, param)
} }
}, },
updateContactStats = { contact ->
withApi {
val r = chatModel.controller.apiContactInfo(chat.chatInfo.apiId)
if (r != null) {
chatModel.updateContactConnectionStats(contact, r.first)
}
}
},
updateMemberStats = { groupInfo, member ->
withApi {
val r = chatModel.controller.apiGroupMemberInfo(groupInfo.groupId, member.groupMemberId)
if (r != null) {
val memStats = r.second
if (memStats != null) {
chatModel.updateGroupMemberConnectionStats(groupInfo, r.first, memStats)
}
}
}
},
syncContactConnection = { contact ->
withApi {
val cStats = chatModel.controller.apiSyncContactRatchet(contact.contactId, force = false)
if (cStats != null) {
chatModel.updateContactConnectionStats(contact, cStats)
}
}
},
syncMemberConnection = { groupInfo, member ->
withApi {
val r = chatModel.controller.apiSyncGroupMemberRatchet(groupInfo.apiId, member.groupMemberId, force = false)
if (r != null) {
chatModel.updateGroupMemberConnectionStats(groupInfo, r.first, r.second)
}
}
},
findModelChat = { chatId ->
chatModel.getChat(chatId)
},
findModelMember = { memberId ->
chatModel.groupMembers.find { it.id == memberId }
},
setReaction = { cInfo, cItem, add, reaction ->
withApi {
val updatedCI = chatModel.controller.apiChatItemReaction(
type = cInfo.chatType,
id = cInfo.apiId,
itemId = cItem.id,
add = add,
reaction = reaction
)
if (updatedCI != null) {
chatModel.updateChatItem(cInfo, updatedCI)
}
}
},
showItemDetails = { cInfo, cItem ->
withApi {
val ciInfo = chatModel.controller.apiGetChatItemInfo(cInfo.chatType, cInfo.apiId, cItem.id)
if (ciInfo != null) {
ModalManager.shared.showModal(endButtons = { ShareButton {
shareText(itemInfoShareText(cItem, ciInfo, chatModel.controller.appPrefs.developerTools.get()))
} }) {
ChatItemInfoView(cItem, ciInfo, devTools = chatModel.controller.appPrefs.developerTools.get())
}
}
}
},
addMembers = { groupInfo -> addMembers = { groupInfo ->
hideKeyboard(view) hideKeyboard(view)
withApi { withApi {
@@ -347,7 +278,7 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: () -> Unit) {
) )
} }
}, },
changeNtfsState = { enabled, currentValue -> toggleNotifications(chat, enabled, chatModel, currentValue) }, changeNtfsState = { enabled, currentValue -> changeNtfsStatePerChat(enabled, currentValue, chat, chatModel) },
onSearchValueChanged = { value -> onSearchValueChanged = { value ->
if (searchText.value == value) return@ChatLayout if (searchText.value == value) return@ChatLayout
val c = chatModel.getChat(chat.chatInfo.id) ?: return@ChatLayout val c = chatModel.getChat(chat.chatInfo.id) ?: return@ChatLayout
@@ -385,14 +316,6 @@ fun ChatLayout(
startCall: (CallMediaType) -> Unit, startCall: (CallMediaType) -> Unit,
acceptCall: (Contact) -> Unit, acceptCall: (Contact) -> Unit,
acceptFeature: (Contact, ChatFeature, Int?) -> Unit, acceptFeature: (Contact, ChatFeature, Int?) -> Unit,
updateContactStats: (Contact) -> Unit,
updateMemberStats: (GroupInfo, GroupMember) -> Unit,
syncContactConnection: (Contact) -> Unit,
syncMemberConnection: (GroupInfo, GroupMember) -> Unit,
findModelChat: (String) -> Chat?,
findModelMember: (String) -> GroupMember?,
setReaction: (ChatInfo, ChatItem, Boolean, MsgReaction) -> Unit,
showItemDetails: (ChatInfo, ChatItem) -> Unit,
addMembers: (GroupInfo) -> Unit, addMembers: (GroupInfo) -> Unit,
markRead: (CC.ItemRange, unreadCountAfter: Int?) -> Unit, markRead: (CC.ItemRange, unreadCountAfter: Int?) -> Unit,
changeNtfsState: (Boolean, currentValue: MutableState<Boolean>) -> Unit, changeNtfsState: (Boolean, currentValue: MutableState<Boolean>) -> Unit,
@@ -403,6 +326,7 @@ fun ChatLayout(
Box( Box(
Modifier Modifier
.fillMaxWidth() .fillMaxWidth()
.themedBackground()
) { ) {
ProvideWindowInsets(windowInsetsAnimationsEnabled = true) { ProvideWindowInsets(windowInsetsAnimationsEnabled = true) {
ModalBottomSheetLayout( ModalBottomSheetLayout(
@@ -428,16 +352,11 @@ fun ChatLayout(
modifier = Modifier.navigationBarsWithImePadding(), modifier = Modifier.navigationBarsWithImePadding(),
floatingActionButton = { floatingButton.value() }, floatingActionButton = { floatingButton.value() },
) { contentPadding -> ) { contentPadding ->
BoxWithConstraints(Modifier BoxWithConstraints(Modifier.fillMaxHeight().padding(contentPadding)) {
.fillMaxHeight()
.padding(contentPadding)
) {
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, cancelFile, joinGroup, acceptCall, acceptFeature, receiveFile, cancelFile, joinGroup, acceptCall, acceptFeature, markRead, setFloatingButton, onComposed,
updateContactStats, updateMemberStats, syncContactConnection, syncMemberConnection, findModelChat, findModelMember,
setReaction, showItemDetails, markRead, setFloatingButton, onComposed,
) )
} }
} }
@@ -471,7 +390,7 @@ 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(MR.strings.search_verb).capitalize(Locale.current), painterResource(MR.images.ic_search), onClick = { ItemAction(stringResource(android.R.string.search_go).capitalize(Locale.current), painterResource(R.drawable.ic_search), onClick = {
showMenu.value = false showMenu.value = false
showSearch = true showSearch = true
}) })
@@ -483,11 +402,11 @@ fun ChatInfoToolbar(
showMenu.value = false showMenu.value = false
startCall(CallMediaType.Audio) startCall(CallMediaType.Audio)
}) { }) {
Icon(painterResource(MR.images.ic_call_500), stringResource(MR.strings.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(MR.strings.icon_descr_video_call).capitalize(Locale.current), painterResource(MR.images.ic_videocam), onClick = { ItemAction(stringResource(R.string.icon_descr_video_call).capitalize(Locale.current), painterResource(R.drawable.ic_videocam), onClick = {
showMenu.value = false showMenu.value = false
startCall(CallMediaType.Video) startCall(CallMediaType.Video)
}) })
@@ -498,15 +417,15 @@ fun ChatInfoToolbar(
showMenu.value = false showMenu.value = false
addMembers(chat.chatInfo.groupInfo) addMembers(chat.chatInfo.groupInfo)
}) { }) {
Icon(painterResource(MR.images.ic_person_add_500), stringResource(MR.strings.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)
} }
} }
} }
val ntfsEnabled = remember { mutableStateOf(chat.chatInfo.ntfsEnabled) } val ntfsEnabled = remember { mutableStateOf(chat.chatInfo.ntfsEnabled) }
menuItems.add { menuItems.add {
ItemAction( ItemAction(
if (ntfsEnabled.value) stringResource(MR.strings.mute_chat) else stringResource(MR.strings.unmute_chat), if (ntfsEnabled.value) stringResource(R.string.mute_chat) else stringResource(R.string.unmute_chat),
if (ntfsEnabled.value) painterResource(MR.images.ic_notifications_off) else painterResource(MR.images.ic_notifications), if (ntfsEnabled.value) painterResource(R.drawable.ic_notifications_off) else painterResource(R.drawable.ic_notifications),
onClick = { onClick = {
showMenu.value = 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
@@ -520,7 +439,7 @@ fun ChatInfoToolbar(
barButtons.add { barButtons.add {
IconButton({ showMenu.value = true }) { IconButton({ showMenu.value = true }) {
Icon(MoreVertFilled, stringResource(MR.strings.icon_descr_more_button), tint = MaterialTheme.colors.primary) Icon(MoreVertFilled, stringResource(R.string.icon_descr_more_button), tint = MaterialTheme.colors.primary)
} }
} }
@@ -577,7 +496,7 @@ fun ChatInfoToolbarTitle(cInfo: ChatInfo, imageSize: Dp = 40.dp, iconColor: Colo
@Composable @Composable
private fun ContactVerifiedShield() { private fun ContactVerifiedShield() {
Icon(painterResource(MR.images.ic_verified_user), null, Modifier.size(18.dp).padding(end = 3.dp, top = 1.dp), tint = MaterialTheme.colors.secondary) 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)
@@ -610,14 +529,6 @@ fun BoxWithConstraintsScope.ChatItemsList(
joinGroup: (Long) -> Unit, joinGroup: (Long) -> Unit,
acceptCall: (Contact) -> Unit, acceptCall: (Contact) -> Unit,
acceptFeature: (Contact, ChatFeature, Int?) -> Unit, acceptFeature: (Contact, ChatFeature, Int?) -> Unit,
updateContactStats: (Contact) -> Unit,
updateMemberStats: (GroupInfo, GroupMember) -> Unit,
syncContactConnection: (Contact) -> Unit,
syncMemberConnection: (GroupInfo, GroupMember) -> Unit,
findModelChat: (String) -> Chat?,
findModelMember: (String) -> GroupMember?,
setReaction: (ChatInfo, ChatItem, Boolean, MsgReaction) -> Unit,
showItemDetails: (ChatInfo, ChatItem) -> Unit,
markRead: (CC.ItemRange, unreadCountAfter: Int?) -> Unit, markRead: (CC.ItemRange, unreadCountAfter: Int?) -> Unit,
setFloatingButton: (@Composable () -> Unit) -> Unit, setFloatingButton: (@Composable () -> Unit) -> Unit,
onComposed: () -> Unit, onComposed: () -> Unit,
@@ -705,43 +616,47 @@ fun BoxWithConstraintsScope.ChatItemsList(
} }
} }
} }
val voiceWithTransparentBack = cItem.content.msgContent is MsgContent.MCVoice && cItem.content.text.isEmpty() && cItem.quotedItem == null
if (chat.chatInfo is ChatInfo.Group) { if (chat.chatInfo is ChatInfo.Group) {
if (cItem.chatDir is CIDirection.GroupRcv) { if (cItem.chatDir is CIDirection.GroupRcv) {
val prevItem = if (i < reversedChatItems.lastIndex) reversedChatItems[i + 1] else null val prevItem = if (i < reversedChatItems.lastIndex) reversedChatItems[i + 1] else null
val member = cItem.chatDir.groupMember val member = cItem.chatDir.groupMember
val showMember = showMemberImage(member, prevItem) val showMember = showMemberImage(member, prevItem)
Row(Modifier.padding(start = 8.dp, end = if (voiceWithTransparentBack) 12.dp else 66.dp).then(swipeableModifier)) { Row(Modifier.padding(start = 8.dp, end = 66.dp).then(swipeableModifier)) {
if (showMember) { if (showMember) {
Box( val contactId = member.memberContactId
Modifier if (contactId == null) {
.clip(CircleShape)
.clickable {
showMemberInfo(chat.chatInfo.groupInfo, member)
}
) {
MemberImage(member) MemberImage(member)
} else {
Box(
Modifier
.clip(CircleShape)
.clickable {
showMemberInfo(chat.chatInfo.groupInfo, member)
}
) {
MemberImage(member)
}
} }
Spacer(Modifier.size(4.dp)) Spacer(Modifier.size(4.dp))
} 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, cancelFile = cancelFile, joinGroup = {}, acceptCall = acceptCall, acceptFeature = acceptFeature, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails) 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 = if (voiceWithTransparentBack) 12.dp else 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, cancelFile = cancelFile, joinGroup = {}, acceptCall = acceptCall, acceptFeature = acceptFeature, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails) 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
val sent = cItem.chatDir.sent val sent = cItem.chatDir.sent
Box( Box(
Modifier.padding( Modifier.padding(
start = if (sent && !voiceWithTransparentBack) 76.dp else 12.dp, start = if (sent) 76.dp else 12.dp,
end = if (sent || voiceWithTransparentBack) 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, cancelFile = cancelFile, joinGroup = joinGroup, acceptCall = acceptCall, acceptFeature = acceptFeature, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails) ChatItemView(chat.chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, deleteMessage = deleteMessage, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = joinGroup, acceptCall = acceptCall, acceptFeature = acceptFeature, scrollToItem = scrollToItem)
} }
} }
@@ -879,8 +794,8 @@ fun BoxWithConstraintsScope.FloatingButtons(
DefaultDropdownMenu(showDropDown, offset = DpOffset(maxWidth - DEFAULT_PADDING, 24.dp + fabSize)) { DefaultDropdownMenu(showDropDown, offset = DpOffset(maxWidth - DEFAULT_PADDING, 24.dp + fabSize)) {
ItemAction( ItemAction(
generalGetString(MR.strings.mark_read), generalGetString(R.string.mark_read),
painterResource(MR.images.ic_check), painterResource(R.drawable.ic_check),
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),
@@ -940,7 +855,6 @@ private fun TopEndFloatingButton(
FloatingActionButton( FloatingActionButton(
{}, // no action here {}, // no action here
modifier.size(48.dp), modifier.size(48.dp),
backgroundColor = MaterialTheme.colors.secondaryVariant,
elevation = FloatingActionButtonDefaults.elevation(0.dp, 0.dp), elevation = FloatingActionButtonDefaults.elevation(0.dp, 0.dp),
interactionSource = interactionSource, interactionSource = interactionSource,
) { ) {
@@ -967,8 +881,7 @@ private fun bottomEndFloatingButton(
FloatingActionButton( FloatingActionButton(
onClick = onClickCounter, onClick = onClickCounter,
elevation = FloatingActionButtonDefaults.elevation(0.dp, 0.dp, 0.dp, 0.dp), elevation = FloatingActionButtonDefaults.elevation(0.dp, 0.dp, 0.dp, 0.dp),
modifier = Modifier.size(48.dp), modifier = Modifier.size(48.dp)
backgroundColor = MaterialTheme.colors.secondaryVariant,
) { ) {
Text( Text(
unreadCountStr(unreadCount), unreadCountStr(unreadCount),
@@ -983,11 +896,10 @@ private fun bottomEndFloatingButton(
FloatingActionButton( FloatingActionButton(
onClick = onClickArrowDown, onClick = onClickArrowDown,
elevation = FloatingActionButtonDefaults.elevation(0.dp, 0.dp, 0.dp, 0.dp), elevation = FloatingActionButtonDefaults.elevation(0.dp, 0.dp, 0.dp, 0.dp),
modifier = Modifier.size(48.dp), modifier = Modifier.size(48.dp)
backgroundColor = MaterialTheme.colors.secondaryVariant,
) { ) {
Icon( Icon(
painter = painterResource(MR.images.ic_keyboard_arrow_down), painter = painterResource(R.drawable.ic_keyboard_arrow_down),
contentDescription = null, contentDescription = null,
tint = MaterialTheme.colors.primary tint = MaterialTheme.colors.primary
) )
@@ -1027,7 +939,7 @@ private fun providerForGallery(
scrollTo: (Int) -> Unit scrollTo: (Int) -> Unit
): ImageGalleryProvider { ): ImageGalleryProvider {
fun canShowMedia(item: ChatItem): Boolean = fun canShowMedia(item: ChatItem): Boolean =
(item.content.msgContent is MsgContent.MCImage || item.content.msgContent is MsgContent.MCVideo) && (item.file?.loaded == true && getLoadedFilePath(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
@@ -1054,15 +966,15 @@ private fun providerForGallery(
val item = item(internalIndex, initialChatId)?.second ?: return null val item = item(internalIndex, initialChatId)?.second ?: return null
return when (item.content.msgContent) { return when (item.content.msgContent) {
is MsgContent.MCImage -> { is MsgContent.MCImage -> {
val imageBitmap: Bitmap? = getLoadedImage(item.file) val imageBitmap: Bitmap? = getLoadedImage(SimplexApp.context, item.file)
val filePath = getLoadedFilePath(item.file) val filePath = getLoadedFilePath(SimplexApp.context, item.file)
if (imageBitmap != null && filePath != null) { if (imageBitmap != null && filePath != null) {
val uri = FileProvider.getUriForFile(SimplexApp.context, "${BuildConfig.APPLICATION_ID}.provider", File(filePath)) val uri = FileProvider.getUriForFile(SimplexApp.context, "${BuildConfig.APPLICATION_ID}.provider", File(filePath))
ProviderMedia.Image(uri, imageBitmap) ProviderMedia.Image(uri, imageBitmap)
} else null } else null
} }
is MsgContent.MCVideo -> { is MsgContent.MCVideo -> {
val filePath = getLoadedFilePath(item.file) val filePath = getLoadedFilePath(SimplexApp.context, item.file)
if (filePath != null) { if (filePath != null) {
val uri = FileProvider.getUriForFile(SimplexApp.context, "${BuildConfig.APPLICATION_ID}.provider", File(filePath)) val uri = FileProvider.getUriForFile(SimplexApp.context, "${BuildConfig.APPLICATION_ID}.provider", File(filePath))
ProviderMedia.Video(uri, (item.content.msgContent as MsgContent.MCVideo).image) ProviderMedia.Video(uri, (item.content.msgContent as MsgContent.MCVideo).image)
@@ -1164,14 +1076,6 @@ fun PreviewChatLayout() {
startCall = {}, startCall = {},
acceptCall = { _ -> }, acceptCall = { _ -> },
acceptFeature = { _, _, _ -> }, acceptFeature = { _, _, _ -> },
updateContactStats = { },
updateMemberStats = { _, _ -> },
syncContactConnection = { },
syncMemberConnection = { _, _ -> },
findModelChat = { null },
findModelMember = { null },
setReaction = { _, _, _, _ -> },
showItemDetails = { _, _ -> },
addMembers = { _ -> }, addMembers = { _ -> },
markRead = { _, _ -> }, markRead = { _, _ -> },
changeNtfsState = { _, _ -> }, changeNtfsState = { _, _ -> },
@@ -1232,14 +1136,6 @@ fun PreviewGroupChatLayout() {
startCall = {}, startCall = {},
acceptCall = { _ -> }, acceptCall = { _ -> },
acceptFeature = { _, _, _ -> }, acceptFeature = { _, _, _ -> },
updateContactStats = { },
updateMemberStats = { _, _ -> },
syncContactConnection = { },
syncMemberConnection = { _, _ -> },
findModelChat = { null },
findModelMember = { null },
setReaction = { _, _, _, _ -> },
showItemDetails = { _, _ -> },
addMembers = { _ -> }, addMembers = { _ -> },
markRead = { _, _ -> }, markRead = { _, _ -> },
changeNtfsState = { _, _ -> }, changeNtfsState = { _, _ -> },
@@ -5,13 +5,12 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState 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 dev.icerock.moko.resources.compose.painterResource import androidx.compose.ui.res.painterResource
import dev.icerock.moko.resources.compose.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.res.MR
@Composable @Composable
fun ComposeFileView(fileName: String, cancelFile: () -> Unit, cancelEnabled: Boolean) { fun ComposeFileView(fileName: String, cancelFile: () -> Unit, cancelEnabled: Boolean) {
@@ -25,8 +24,8 @@ fun ComposeFileView(fileName: String, cancelFile: () -> Unit, cancelEnabled: Boo
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Icon( Icon(
painterResource(MR.images.ic_draft_filled), painterResource(R.drawable.ic_draft_filled),
stringResource(MR.strings.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),
@@ -37,8 +36,8 @@ 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(
painterResource(MR.images.ic_close), painterResource(R.drawable.ic_close),
contentDescription = stringResource(MR.strings.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)
) )
@@ -11,14 +11,13 @@ 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.asImageBitmap import androidx.compose.ui.graphics.asImageBitmap
import dev.icerock.moko.resources.compose.painterResource import androidx.compose.ui.res.painterResource
import dev.icerock.moko.resources.compose.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.* import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.helpers.UploadContent import chat.simplex.app.views.helpers.UploadContent
import chat.simplex.app.views.helpers.base64ToBitmap import chat.simplex.app.views.helpers.base64ToBitmap
import chat.simplex.res.MR
@Composable @Composable
fun ComposeImageView(media: ComposePreview.MediaPreview, cancelImages: () -> Unit, cancelEnabled: Boolean) { fun ComposeImageView(media: ComposePreview.MediaPreview, cancelImages: () -> Unit, cancelEnabled: Boolean) {
@@ -45,7 +44,7 @@ fun ComposeImageView(media: ComposePreview.MediaPreview, cancelImages: () -> Uni
modifier = Modifier.widthIn(max = 80.dp).height(60.dp) modifier = Modifier.widthIn(max = 80.dp).height(60.dp)
) )
Icon( Icon(
painterResource(MR.images.ic_videocam_filled), painterResource(R.drawable.ic_videocam_filled),
"preview video", "preview video",
Modifier Modifier
.size(20.dp), .size(20.dp),
@@ -65,8 +64,8 @@ fun ComposeImageView(media: ComposePreview.MediaPreview, cancelImages: () -> Uni
if (cancelEnabled) { if (cancelEnabled) {
IconButton(onClick = cancelImages) { IconButton(onClick = cancelImages) {
Icon( Icon(
painterResource(MR.images.ic_close), painterResource(R.drawable.ic_close),
contentDescription = stringResource(MR.strings.icon_descr_cancel_image_preview), contentDescription = stringResource(R.string.icon_descr_cancel_image_preview),
tint = MaterialTheme.colors.primary, tint = MaterialTheme.colors.primary,
) )
} }
@@ -16,7 +16,6 @@ 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.clickable
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.*
@@ -27,8 +26,8 @@ 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 dev.icerock.moko.resources.compose.painterResource import androidx.compose.ui.res.painterResource
import dev.icerock.moko.resources.compose.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 chat.simplex.app.* import chat.simplex.app.*
@@ -36,7 +35,6 @@ import chat.simplex.app.R
import chat.simplex.app.model.* import chat.simplex.app.model.*
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 chat.simplex.res.MR
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.serialization.* import kotlinx.serialization.*
@@ -120,7 +118,7 @@ data class ComposeState(
val attachmentDisabled: Boolean val attachmentDisabled: Boolean
get() { get() {
if (editing || liveMessage != null || inProgress) return true if (editing || liveMessage != null) return true
return when (preview) { return when (preview) {
ComposePreview.NoPreview -> false ComposePreview.NoPreview -> false
is ComposePreview.CLinkPreview -> false is ComposePreview.CLinkPreview -> false
@@ -194,7 +192,7 @@ fun ComposeView(
if (isGranted) { if (isGranted) {
cameraLauncher.launchWithFallback() cameraLauncher.launchWithFallback()
} else { } else {
Toast.makeText(context, generalGetString(MR.strings.toast_permission_denied), Toast.LENGTH_SHORT).show() Toast.makeText(context, generalGetString(R.string.toast_permission_denied), Toast.LENGTH_SHORT).show()
} }
} }
val processPickedMedia = { uris: List<Uri>, text: String? -> val processPickedMedia = { uris: List<Uri>, text: String? ->
@@ -202,7 +200,7 @@ fun ComposeView(
val imagesPreview = ArrayList<String>() val imagesPreview = ArrayList<String>()
uris.forEach { uri -> uris.forEach { uri ->
var bitmap: Bitmap? = null var bitmap: Bitmap? = null
val isImage = MimeTypeMap.getSingleton().getMimeTypeFromExtension(getFileName(uri)?.split(".")?.last())?.contains("image/") == true val isImage = MimeTypeMap.getSingleton().getMimeTypeFromExtension(getFileName(SimplexApp.context, uri)?.split(".")?.last())?.contains("image/") == true
when { when {
isImage -> { isImage -> {
// Image // Image
@@ -210,17 +208,17 @@ fun ComposeView(
bitmap = if (drawable != null) getBitmapFromUri(uri) else null bitmap = if (drawable != null) getBitmapFromUri(uri) else null
val isAnimNewApi = Build.VERSION.SDK_INT >= 28 && drawable is AnimatedImageDrawable val isAnimNewApi = Build.VERSION.SDK_INT >= 28 && drawable is AnimatedImageDrawable
val isAnimOldApi = Build.VERSION.SDK_INT < 28 && val isAnimOldApi = Build.VERSION.SDK_INT < 28 &&
(getFileName(uri)?.endsWith(".gif") == true || getFileName(uri)?.endsWith(".webp") == true) (getFileName(SimplexApp.context, uri)?.endsWith(".gif") == true || getFileName(SimplexApp.context, uri)?.endsWith(".webp") == true)
if (isAnimNewApi || isAnimOldApi) { if (isAnimNewApi || isAnimOldApi) {
// It's a gif or webp // It's a gif or webp
val fileSize = getFileSize(uri) val fileSize = getFileSize(context, uri)
if (fileSize != null && fileSize <= maxFileSize) { if (fileSize != null && fileSize <= maxFileSize) {
content.add(UploadContent.AnimatedImage(uri)) content.add(UploadContent.AnimatedImage(uri))
} else { } else {
bitmap = null bitmap = null
AlertManager.shared.showAlertMsg( AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.large_file), generalGetString(R.string.large_file),
String.format(generalGetString(MR.strings.maximum_supported_file_size), formatBytes(maxFileSize)) String.format(generalGetString(R.string.maximum_supported_file_size), formatBytes(maxFileSize))
) )
} }
} else { } else {
@@ -245,16 +243,16 @@ fun ComposeView(
} }
val processPickedFile = { uri: Uri?, text: String? -> val processPickedFile = { uri: Uri?, text: String? ->
if (uri != null) { if (uri != null) {
val fileSize = getFileSize(uri) val fileSize = getFileSize(context, uri)
if (fileSize != null && fileSize <= maxFileSize) { if (fileSize != null && fileSize <= maxFileSize) {
val fileName = getFileName(uri) val fileName = getFileName(SimplexApp.context, uri)
if (fileName != null) { if (fileName != null) {
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, uri))
} }
} else { } else {
AlertManager.shared.showAlertMsg( AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.large_file), generalGetString(R.string.large_file),
String.format(generalGetString(MR.strings.maximum_supported_file_size), formatBytes(maxFileSize)) String.format(generalGetString(R.string.maximum_supported_file_size), formatBytes(maxFileSize))
) )
} }
} }
@@ -368,27 +366,22 @@ fun ComposeView(
chatModel.filesToDelete.clear() chatModel.filesToDelete.clear()
} }
suspend fun send(cInfo: ChatInfo, mc: MsgContent, quoted: Long?, file: String? = null, live: Boolean = false, ttl: Int?): 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,
id = cInfo.apiId, id = cInfo.apiId,
file = file, file = file,
quotedItemId = quoted, quotedItemId = quoted,
mc = mc, mc = mc,
live = live, live = live
ttl = ttl
) )
if (aChatItem != null) { if (aChatItem != null) chatModel.addChatItem(cInfo, aChatItem.chatItem)
chatModel.addChatItem(cInfo, aChatItem.chatItem) return aChatItem?.chatItem
return aChatItem.chatItem
}
if (file != null) removeFile(file)
return null
} }
suspend fun sendMessageAsync(text: String?, live: Boolean, ttl: Int?): ChatItem? { suspend fun sendMessageAsync(text: String?, live: Boolean): ChatItem? {
val cInfo = chat.chatInfo val cInfo = chat.chatInfo
val cs = composeState.value val cs = composeState.value
var sent: ChatItem? var sent: ChatItem?
@@ -461,9 +454,9 @@ fun ComposeView(
is ComposePreview.MediaPreview -> { is ComposePreview.MediaPreview -> {
preview.content.forEachIndexed { index, it -> preview.content.forEachIndexed { index, it ->
val file = when (it) { val file = when (it) {
is UploadContent.SimpleImage -> saveImage(it.uri) is UploadContent.SimpleImage -> saveImage(context, it.uri)
is UploadContent.AnimatedImage -> saveAnimImage(it.uri) is UploadContent.AnimatedImage -> saveAnimImage(context, it.uri)
is UploadContent.Video -> saveFileFromUri(it.uri) is UploadContent.Video -> saveFileFromUri(context, it.uri)
} }
if (file != null) { if (file != null) {
files.add(file) files.add(file)
@@ -478,7 +471,7 @@ fun ComposeView(
is ComposePreview.VoicePreview -> { is ComposePreview.VoicePreview -> {
val tmpFile = File(preview.voice) val tmpFile = File(preview.voice)
AudioPlayer.stop(tmpFile.absolutePath) AudioPlayer.stop(tmpFile.absolutePath)
val actualFile = File(getAppFilePath(tmpFile.name.replaceAfter(RecorderNative.extension, ""))) val actualFile = File(getAppFilePath(SimplexApp.context, tmpFile.name.replaceAfter(RecorderNative.extension, "")))
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
Files.move(tmpFile.toPath(), actualFile.toPath()) Files.move(tmpFile.toPath(), actualFile.toPath())
} }
@@ -487,7 +480,7 @@ fun ComposeView(
msgs.add(MsgContent.MCVoice(if (msgs.isEmpty()) msgText else "", preview.durationMs / 1000)) msgs.add(MsgContent.MCVoice(if (msgs.isEmpty()) msgText else "", preview.durationMs / 1000))
} }
is ComposePreview.FilePreview -> { is ComposePreview.FilePreview -> {
val file = saveFileFromUri(preview.uri) val file = saveFileFromUri(context, preview.uri)
if (file != null) { if (file != null) {
files.add((file)) files.add((file))
msgs.add(MsgContent.MCFile(if (msgs.isEmpty()) msgText else "")) msgs.add(MsgContent.MCFile(if (msgs.isEmpty()) msgText else ""))
@@ -502,8 +495,7 @@ fun ComposeView(
msgs.forEachIndexed { index, content -> msgs.forEachIndexed { index, content ->
if (index > 0) delay(100) if (index > 0) delay(100)
sent = send(cInfo, content, if (index == 0) quotedItemId else null, files.getOrNull(index), sent = send(cInfo, content, if (index == 0) quotedItemId else null, files.getOrNull(index),
live = if (content !is MsgContent.MCVoice && index == msgs.lastIndex) live else false, if (content !is MsgContent.MCVoice && index == msgs.lastIndex) live else false
ttl = ttl
) )
} }
if (sent == null && if (sent == null &&
@@ -511,16 +503,16 @@ fun ComposeView(
cs.preview is ComposePreview.FilePreview || cs.preview is ComposePreview.FilePreview ||
cs.preview is ComposePreview.VoicePreview) cs.preview is ComposePreview.VoicePreview)
) { ) {
sent = send(cInfo, MsgContent.MCText(msgText), quotedItemId, null, live, ttl) sent = send(cInfo, MsgContent.MCText(msgText), quotedItemId, null, live)
} }
} }
clearState(live) clearState(live)
return sent return sent
} }
fun sendMessage(ttl: Int?) { fun sendMessage() {
withBGApi { withBGApi {
sendMessageAsync(null, false, ttl) sendMessageAsync(null, false)
} }
} }
@@ -595,8 +587,8 @@ fun ComposeView(
suspend fun sendLiveMessage() { suspend fun sendLiveMessage() {
val cs = composeState.value val cs = composeState.value
val typedMsg = cs.message val typedMsg = cs.message
if ((cs.sendEnabled() || cs.contextItem is ComposeContextItem.QuotedItem) && (cs.liveMessage == null || !cs.liveMessage.sent)) { if ((cs.sendEnabled() || cs.contextItem is ComposeContextItem.QuotedItem) && (cs.liveMessage == null || !cs.liveMessage?.sent)) {
val ci = sendMessageAsync(typedMsg, live = true, ttl = null) val ci = sendMessageAsync(typedMsg, live = true)
if (ci != null) { if (ci != null) {
composeState.value = composeState.value.copy(liveMessage = LiveMessage(ci, typedMsg = typedMsg, sentMsg = typedMsg, sent = true)) composeState.value = composeState.value.copy(liveMessage = LiveMessage(ci, typedMsg = typedMsg, sentMsg = typedMsg, sent = true))
} }
@@ -617,7 +609,7 @@ fun ComposeView(
if (liveMessage != null) { if (liveMessage != null) {
val sentMsg = liveMessageToSend(liveMessage, typedMsg) val sentMsg = liveMessageToSend(liveMessage, typedMsg)
if (sentMsg != null) { if (sentMsg != null) {
val ci = sendMessageAsync(sentMsg, live = true, ttl = null) val ci = sendMessageAsync(sentMsg, live = true)
if (ci != null) { if (ci != null) {
composeState.value = composeState.value.copy(liveMessage = LiveMessage(ci, typedMsg = typedMsg, sentMsg = sentMsg, sent = true)) composeState.value = composeState.value.copy(liveMessage = LiveMessage(ci, typedMsg = typedMsg, sentMsg = sentMsg, sent = true))
} }
@@ -631,27 +623,23 @@ fun ComposeView(
fun previewView() { fun previewView() {
when (val preview = composeState.value.preview) { when (val preview = composeState.value.preview) {
ComposePreview.NoPreview -> {} ComposePreview.NoPreview -> {}
is ComposePreview.CLinkPreview -> ComposeLinkView( is ComposePreview.CLinkPreview -> ComposeLinkView(preview.linkPreview, ::cancelLinkPreview)
preview.linkPreview,
::cancelLinkPreview,
cancelEnabled = !composeState.value.inProgress
)
is ComposePreview.MediaPreview -> ComposeImageView( is ComposePreview.MediaPreview -> ComposeImageView(
preview, preview,
::cancelImages, ::cancelImages,
cancelEnabled = !composeState.value.editing && !composeState.value.inProgress cancelEnabled = !composeState.value.editing
) )
is ComposePreview.VoicePreview -> ComposeVoiceView( is ComposePreview.VoicePreview -> ComposeVoiceView(
preview.voice, preview.voice,
preview.durationMs, preview.durationMs,
preview.finished, preview.finished,
cancelEnabled = !composeState.value.editing && !composeState.value.inProgress, cancelEnabled = !composeState.value.editing,
::cancelVoice ::cancelVoice
) )
is ComposePreview.FilePreview -> ComposeFileView( is ComposePreview.FilePreview -> ComposeFileView(
preview.fileName, preview.fileName,
::cancelFile, ::cancelFile,
cancelEnabled = !composeState.value.editing && !composeState.value.inProgress cancelEnabled = !composeState.value.editing
) )
} }
} }
@@ -660,23 +648,15 @@ 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, painterResource(MR.images.ic_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, painterResource(MR.images.ic_edit_filled)) { is ComposeContextItem.EditingItem -> ContextItemView(contextItem.chatItem, painterResource(R.drawable.ic_edit_filled)) {
clearState() clearState()
} }
} }
} }
// In case a user sent something, state is in progress, the user rotates a screen to different orientation.
// Without clearing the state the user will be unable to send anything until re-enters ChatView
LaunchedEffect(Unit) {
if (composeState.value.inProgress) {
clearState()
}
}
LaunchedEffect(chatModel.sharedContent.value) { LaunchedEffect(chatModel.sharedContent.value) {
// Important. If it's null, don't do anything, chat is not closed yet but will be after a moment // Important. If it's null, don't do anything, chat is not closed yet but will be after a moment
if (chatModel.chatId.value == null) return@LaunchedEffect if (chatModel.chatId.value == null) return@LaunchedEffect
@@ -694,43 +674,21 @@ fun ComposeView(
val userIsObserver = rememberUpdatedState(chat.userIsObserver) val userIsObserver = rememberUpdatedState(chat.userIsObserver)
Column { Column {
if (composeState.value.preview !is ComposePreview.VoicePreview || composeState.value.editing) { contextItemView()
contextItemView() when {
when { composeState.value.editing && composeState.value.preview is ComposePreview.VoicePreview -> {}
composeState.value.editing && composeState.value.preview is ComposePreview.VoicePreview -> {} composeState.value.editing && composeState.value.preview is ComposePreview.FilePreview -> {}
composeState.value.editing && composeState.value.preview is ComposePreview.FilePreview -> {} else -> previewView()
else -> previewView()
}
} else {
Box {
Box(Modifier.align(Alignment.TopStart).padding(bottom = 69.dp)) {
contextItemView()
}
Box(Modifier.align(Alignment.BottomStart)) {
previewView()
}
}
} }
Row( Row(
modifier = Modifier.padding(end = 8.dp), modifier = Modifier.padding(end = 8.dp),
verticalAlignment = Alignment.Bottom, verticalAlignment = Alignment.Bottom,
) { ) {
val isGroupAndProhibitedFiles = chat.chatInfo is ChatInfo.Group && !chat.chatInfo.groupInfo.fullGroupPreferences.files.on IconButton(showChooseAttachment, enabled = !composeState.value.attachmentDisabled && rememberUpdatedState(chat.userCanSend).value) {
val attachmentClicked = if (isGroupAndProhibitedFiles) {
{
AlertManager.shared.showAlertMsg(
title = generalGetString(MR.strings.files_and_media_prohibited),
text = generalGetString(MR.strings.only_owners_can_enable_files_and_media)
)
}
} else {
showChooseAttachment
}
IconButton(attachmentClicked, enabled = !composeState.value.attachmentDisabled && rememberUpdatedState(chat.userCanSend).value) {
Icon( Icon(
painterResource(MR.images.ic_attach_file_filled_500), painterResource(R.drawable.ic_attach_file_filled_500),
contentDescription = stringResource(MR.strings.attach), contentDescription = stringResource(R.string.attach),
tint = if (!composeState.value.attachmentDisabled && userCanSend.value && !isGroupAndProhibitedFiles) MaterialTheme.colors.primary else MaterialTheme.colors.secondary, 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)
@@ -782,12 +740,10 @@ fun ComposeView(
if (orientation == activity.resources.configuration.orientation) { if (orientation == activity.resources.configuration.orientation) {
val cs = composeState.value val cs = composeState.value
if (cs.liveMessage != null && (cs.message.isNotEmpty() || cs.liveMessage.sent)) { if (cs.liveMessage != null && (cs.message.isNotEmpty() || cs.liveMessage.sent)) {
sendMessage(null) sendMessage()
resetLinkPreview() resetLinkPreview()
clearCurrentDraft() clearCurrentDraft()
deleteUnusedFiles() deleteUnusedFiles()
} else if (composeState.value.inProgress) {
clearCurrentDraft()
} else if (!composeState.value.empty) { } else if (!composeState.value.empty) {
if (cs.preview is ComposePreview.VoicePreview && !cs.preview.finished) { if (cs.preview is ComposePreview.VoicePreview && !cs.preview.finished) {
composeState.value = cs.copy(preview = cs.preview.copy(finished = true)) composeState.value = cs.copy(preview = cs.preview.copy(finished = true))
@@ -803,7 +759,6 @@ fun ComposeView(
} }
} }
val timedMessageAllowed = remember(chat.chatInfo) { chat.chatInfo.featureEnabled(ChatFeature.TimedMessages) }
SendMsgView( SendMsgView(
composeState, composeState,
showVoiceRecordIcon = true, showVoiceRecordIcon = true,
@@ -815,10 +770,8 @@ fun ComposeView(
allowVoiceToContact = ::allowVoiceToContact, allowVoiceToContact = ::allowVoiceToContact,
userIsObserver = userIsObserver.value, userIsObserver = userIsObserver.value,
userCanSend = userCanSend.value, userCanSend = userCanSend.value,
timedMessageAllowed = timedMessageAllowed, sendMessage = {
customDisappearingMessageTimePref = chatModel.controller.appPrefs.customDisappearingMessageTime, sendMessage()
sendMessage = { ttl ->
sendMessage(ttl)
resetLinkPreview() resetLinkPreview()
}, },
sendLiveMessage = ::sendLiveMessage, sendLiveMessage = ::sendLiveMessage,
@@ -861,7 +814,7 @@ class PickMultipleImagesFromGallery: ActivityResultContract<Int, List<Uri>>() {
if (uri != null) uris.add(uri) if (uri != null) uris.add(uri)
} }
if (itemCount > 10) { if (itemCount > 10) {
AlertManager.shared.showAlertMsg(MR.strings.images_limit_title, MR.strings.images_limit_desc) AlertManager.shared.showAlertMsg(R.string.images_limit_title, R.string.images_limit_desc)
} }
uris uris
} }
@@ -888,7 +841,7 @@ class PickMultipleVideosFromGallery: ActivityResultContract<Int, List<Uri>>() {
if (uri != null) uris.add(uri) if (uri != null) uris.add(uri)
} }
if (itemCount > 10) { if (itemCount > 10) {
AlertManager.shared.showAlertMsg(MR.strings.videos_limit_title, MR.strings.videos_limit_desc) AlertManager.shared.showAlertMsg(R.string.videos_limit_title, R.string.videos_limit_desc)
} }
uris uris
} }
@@ -0,0 +1,131 @@
import androidx.compose.animation.core.Animatable
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import chat.simplex.app.R
import chat.simplex.app.model.durationText
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.helpers.*
import kotlinx.coroutines.flow.distinctUntilChanged
@Composable
fun ComposeVoiceView(
filePath: String,
recordedDurationMs: Int,
finishedRecording: Boolean,
cancelEnabled: Boolean,
cancelVoice: () -> Unit
) {
BoxWithConstraints(Modifier
.fillMaxWidth()
) {
val audioPlaying = rememberSaveable { mutableStateOf(false) }
val progress = rememberSaveable { mutableStateOf(0) }
val duration = rememberSaveable(recordedDurationMs) { mutableStateOf(recordedDurationMs) }
val progressBarWidth = remember { Animatable(0f) }
LaunchedEffect(recordedDurationMs, finishedRecording) {
snapshotFlow { progress.value }
.distinctUntilChanged()
.collect {
val startTime = when {
finishedRecording -> progress.value
else -> recordedDurationMs
}
val endTime = when {
finishedRecording -> duration.value
audioPlaying.value -> recordedDurationMs
else -> MAX_VOICE_MILLIS_FOR_SENDING
}
val to = ((startTime.toDouble() / endTime) * maxWidth.value).dp
progressBarWidth.animateTo(to.value, audioProgressBarAnimationSpec())
}
}
Spacer(
Modifier
.requiredWidth(progressBarWidth.value.dp)
.padding(top = 58.dp)
.height(3.dp)
.background(MaterialTheme.colors.primary)
)
val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage
Row(
Modifier
.height(60.dp)
.fillMaxWidth()
.padding(top = 8.dp)
.background(sentColor),
verticalAlignment = Alignment.CenterVertically
) {
IconButton(
onClick = {
if (!audioPlaying.value) {
AudioPlayer.play(filePath, audioPlaying, progress, duration, false)
} else {
AudioPlayer.pause(audioPlaying, progress)
}
},
enabled = finishedRecording) {
Icon(
if (audioPlaying.value) painterResource(R.drawable.ic_pause_filled) else painterResource(R.drawable.ic_play_arrow_filled),
stringResource(R.string.icon_descr_file),
Modifier
.padding(start = 4.dp, end = 2.dp)
.size(36.dp),
tint = if (finishedRecording) MaterialTheme.colors.primary else MaterialTheme.colors.secondary
)
}
val numberInText = remember(recordedDurationMs, progress.value) {
derivedStateOf {
when {
finishedRecording && progress.value == 0 && !audioPlaying.value -> duration.value / 1000
finishedRecording -> progress.value / 1000
else -> recordedDurationMs / 1000
}
}
}
Text(
durationText(numberInText.value),
fontSize = 18.sp,
color = MaterialTheme.colors.secondary,
)
Spacer(Modifier.weight(1f))
if (cancelEnabled) {
IconButton(
onClick = {
AudioPlayer.stop(filePath)
cancelVoice()
},
modifier = Modifier.padding(0.dp)
) {
Icon(
painterResource(R.drawable.ic_close),
contentDescription = stringResource(R.string.icon_descr_cancel_file_preview),
tint = MaterialTheme.colors.primary,
modifier = Modifier.padding(10.dp)
)
}
}
}
}
}
@Preview
@Composable
fun PreviewComposeAudioView() {
SimpleXTheme {
ComposeFileView(
"test.txt",
cancelFile = {},
cancelEnabled = true
)
}
}
@@ -14,13 +14,12 @@ import androidx.compose.runtime.*
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.graphics.Color import androidx.compose.ui.graphics.Color
import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.res.stringResource
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.helpers.* import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.usersettings.PreferenceToggle import chat.simplex.app.views.usersettings.PreferenceToggle
import chat.simplex.res.MR
@Composable @Composable
fun ContactPreferencesView( fun ContactPreferencesView(
@@ -82,7 +81,7 @@ private fun ContactPreferencesLayout(
.fillMaxWidth() .fillMaxWidth()
.verticalScroll(rememberScrollState()), .verticalScroll(rememberScrollState()),
) { ) {
AppBarTitle(stringResource(MR.strings.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)) applyPrefs(featuresAllowed.copy(timedMessagesTTL = ttl))
@@ -96,11 +95,6 @@ private fun ContactPreferencesLayout(
applyPrefs(featuresAllowed.copy(fullDelete = it)) applyPrefs(featuresAllowed.copy(fullDelete = it))
} }
SectionDividerSpaced(true, maxBottomPadding = false) SectionDividerSpaced(true, maxBottomPadding = false)
val allowReactions: MutableState<ContactFeatureAllowed> = remember(featuresAllowed) { mutableStateOf(featuresAllowed.reactions) }
FeatureSection(ChatFeature.Reactions, user.fullPreferences.reactions.allow, contact.mergedPreferences.reactions, allowReactions) {
applyPrefs(featuresAllowed.copy(reactions = it))
}
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))
@@ -141,18 +135,19 @@ private fun FeatureSection(
leadingIcon = true, leadingIcon = true,
) { ) {
ExposedDropDownSettingRow( ExposedDropDownSettingRow(
generalGetString(MR.strings.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
) )
InfoRow( InfoRow(
generalGetString(MR.strings.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
@@ -176,28 +171,20 @@ private fun TimedMessagesFeatureSection(
leadingIcon = true, leadingIcon = true,
) { ) {
PreferenceToggle( PreferenceToggle(
generalGetString(MR.strings.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)
} }
InfoRow( InfoRow(
generalGetString(MR.strings.chat_preferences_contact_allows), generalGetString(R.string.chat_preferences_contact_allows),
pref.contactPreference.allow.text pref.contactPreference.allow.text
) )
if (featuresAllowed.timedMessagesAllowed) { if (featuresAllowed.timedMessagesAllowed) {
val ttl = rememberSaveable(featuresAllowed.timedMessagesTTL) { mutableStateOf(featuresAllowed.timedMessagesTTL) } val ttl = rememberSaveable(featuresAllowed.timedMessagesTTL) { mutableStateOf(featuresAllowed.timedMessagesTTL) }
DropdownCustomTimePickerSettingRow( TimedMessagesTTLPicker(ttl, onTTLUpdated)
selection = ttl,
propagateExternalSelectionUpdate = true, // for Reset
label = generalGetString(MR.strings.delete_after),
dropdownValues = TimedMessagesPreference.ttlValues,
customPickerTitle = generalGetString(MR.strings.delete_after),
customPickerConfirmButtonText = generalGetString(MR.strings.custom_time_picker_select),
onSelected = onTTLUpdated
)
} else if (pref.contactPreference.allow == FeatureAllowed.YES || pref.contactPreference.allow == FeatureAllowed.ALWAYS) { } else if (pref.contactPreference.allow == FeatureAllowed.YES || pref.contactPreference.allow == FeatureAllowed.ALWAYS) {
InfoRow(generalGetString(MR.strings.delete_after), timeText(pref.contactPreference.ttl)) InfoRow(generalGetString(R.string.delete_after), TimedMessagesPreference.ttlText(pref.contactPreference.ttl))
} }
} }
SectionTextFooter(ChatFeature.TimedMessages.enabledDescription(enabled)) SectionTextFooter(ChatFeature.TimedMessages.enabledDescription(enabled))
@@ -207,19 +194,31 @@ 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(MR.strings.reset_verb), color = if (disabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary) Text(stringResource(R.string.reset_verb), color = if (disabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary)
} }
SectionItemView(save, disabled = disabled) { SectionItemView(save, disabled = disabled) {
Text(stringResource(MR.strings.save_and_notify_contact), color = if (disabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary) Text(stringResource(R.string.save_and_notify_contact), color = if (disabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary)
} }
} }
} }
@Composable
fun TimedMessagesTTLPicker(selection: MutableState<Int?>, onSelected: (Int?) -> Unit) {
val ttlValues = TimedMessagesPreference.ttlValues
val values = ttlValues + if (ttlValues.contains(selection.value)) listOf() else listOf(selection.value)
ExposedDropDownSettingRow(
generalGetString(R.string.delete_after),
values.map { it to TimedMessagesPreference.ttlText(it) },
selection,
onSelected = onSelected
)
}
private fun showUnsavedChangesAlert(save: () -> Unit, revert: () -> Unit) { private fun showUnsavedChangesAlert(save: () -> Unit, revert: () -> Unit) {
AlertManager.shared.showAlertDialogStacked( AlertManager.shared.showAlertDialogStacked(
title = generalGetString(MR.strings.save_preferences_question), title = generalGetString(R.string.save_preferences_question),
confirmText = generalGetString(MR.strings.save_and_notify_contact), confirmText = generalGetString(R.string.save_and_notify_contact),
dismissText = generalGetString(MR.strings.exit_without_saving), dismissText = generalGetString(R.string.exit_without_saving),
onConfirm = save, onConfirm = save,
onDismiss = revert, onDismiss = revert,
) )
@@ -8,15 +8,14 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState 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 dev.icerock.moko.resources.compose.painterResource import androidx.compose.ui.res.painterResource
import dev.icerock.moko.resources.compose.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.* import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.chat.item.* import chat.simplex.app.views.chat.item.*
import chat.simplex.res.MR
import kotlinx.datetime.Clock import kotlinx.datetime.Clock
@Composable @Composable
@@ -47,7 +46,7 @@ fun ContextItemView(
.padding(horizontal = 8.dp) .padding(horizontal = 8.dp)
.height(20.dp) .height(20.dp)
.width(20.dp), .width(20.dp),
contentDescription = stringResource(MR.strings.icon_descr_context), contentDescription = stringResource(R.string.icon_descr_context),
tint = MaterialTheme.colors.secondary, tint = MaterialTheme.colors.secondary,
) )
MarkdownText( MarkdownText(
@@ -59,8 +58,8 @@ fun ContextItemView(
} }
IconButton(onClick = cancelContextItem) { IconButton(onClick = cancelContextItem) {
Icon( Icon(
painterResource(MR.images.ic_close), painterResource(R.drawable.ic_close),
contentDescription = stringResource(MR.strings.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)
) )
@@ -74,7 +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 = painterResource(MR.images.ic_edit_filled) contextIcon = painterResource(R.drawable.ic_edit_filled)
) {} ) {}
} }
} }
@@ -6,13 +6,12 @@ import androidx.compose.material.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.res.stringResource
import chat.simplex.app.R import chat.simplex.app.R
import chat.simplex.app.ui.theme.DEFAULT_PADDING import chat.simplex.app.ui.theme.DEFAULT_PADDING
import chat.simplex.app.views.helpers.* import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.newchat.QRCodeScanner import chat.simplex.app.views.newchat.QRCodeScanner
import com.google.accompanist.permissions.rememberPermissionState import com.google.accompanist.permissions.rememberPermissionState
import chat.simplex.res.MR
@Composable @Composable
fun ScanCodeView(verifyCode: (String?, cb: (Boolean) -> Unit) -> Unit, close: () -> Unit) { fun ScanCodeView(verifyCode: (String?, cb: (Boolean) -> Unit) -> Unit, close: () -> Unit) {
@@ -30,7 +29,7 @@ private fun ScanCodeLayout(verifyCode: (String?, cb: (Boolean) -> Unit) -> Unit,
.fillMaxSize() .fillMaxSize()
.padding(horizontal = DEFAULT_PADDING) .padding(horizontal = DEFAULT_PADDING)
) { ) {
AppBarTitle(stringResource(MR.strings.scan_code), false) AppBarTitle(stringResource(R.string.scan_code), false)
Box( Box(
Modifier Modifier
.fillMaxWidth() .fillMaxWidth()
@@ -43,12 +42,12 @@ private fun ScanCodeLayout(verifyCode: (String?, cb: (Boolean) -> Unit) -> Unit,
close() close()
} else { } else {
AlertManager.shared.showAlertMsg( AlertManager.shared.showAlertMsg(
title = generalGetString(MR.strings.incorrect_code) title = generalGetString(R.string.incorrect_code)
) )
} }
} }
} }
} }
Text(stringResource(MR.strings.scan_code_from_contacts_app)) Text(stringResource(R.string.scan_code_from_contacts_app))
} }
} }
@@ -18,7 +18,7 @@ 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.* import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.* import androidx.compose.material.*
import androidx.compose.material.ripple.rememberRipple import androidx.compose.material.ripple.rememberRipple
import androidx.compose.runtime.* import androidx.compose.runtime.*
@@ -29,14 +29,14 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.* import androidx.compose.ui.graphics.*
import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.platform.* import androidx.compose.ui.platform.*
import androidx.compose.ui.res.* import androidx.compose.ui.res.painterResource
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
import androidx.compose.ui.text.font.* import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.* import androidx.compose.ui.unit.*
import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.viewinterop.AndroidView
import androidx.compose.ui.window.Dialog
import androidx.core.graphics.drawable.DrawableCompat import androidx.core.graphics.drawable.DrawableCompat
import androidx.core.view.inputmethod.EditorInfoCompat import androidx.core.view.inputmethod.EditorInfoCompat
import androidx.core.view.inputmethod.InputConnectionCompat import androidx.core.view.inputmethod.InputConnectionCompat
@@ -44,14 +44,11 @@ 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.* import chat.simplex.app.ui.theme.CurrentColors
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 chat.simplex.res.MR
import dev.icerock.moko.resources.StringResource
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
import kotlinx.coroutines.* import kotlinx.coroutines.*
import java.lang.reflect.Field import java.lang.reflect.Field
@@ -67,25 +64,13 @@ fun SendMsgView(
userIsObserver: Boolean, userIsObserver: Boolean,
userCanSend: Boolean, userCanSend: Boolean,
allowVoiceToContact: () -> Unit, allowVoiceToContact: () -> Unit,
timedMessageAllowed: Boolean = false, sendMessage: () -> Unit,
customDisappearingMessageTimePref: SharedPreference<Int>? = null,
sendMessage: (Int?) -> Unit,
sendLiveMessage: (suspend () -> Unit)? = null, sendLiveMessage: (suspend () -> Unit)? = null,
updateLiveMessage: (suspend () -> Unit)? = null, updateLiveMessage: (suspend () -> Unit)? = null,
cancelLiveMessage: (() -> Unit)? = null, cancelLiveMessage: (() -> Unit)? = null,
onMessageChange: (String) -> Unit, onMessageChange: (String) -> Unit,
textStyle: MutableState<TextStyle> textStyle: MutableState<TextStyle>
) { ) {
val showCustomDisappearingMessageDialog = remember { mutableStateOf(false) }
if (showCustomDisappearingMessageDialog.value) {
CustomDisappearingMessageDialog(
sendMessage = sendMessage,
setShowDialog = { showCustomDisappearingMessageDialog.value = it },
customDisappearingMessageTimePref = customDisappearingMessageTimePref
)
}
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.MediaPreview || cs.preview is ComposePreview.FilePreview) val showProgress = cs.inProgress && (cs.preview is ComposePreview.MediaPreview || cs.preview is ComposePreview.FilePreview)
@@ -94,16 +79,15 @@ fun SendMsgView(
val showDeleteTextButton = rememberSaveable { mutableStateOf(false) } val showDeleteTextButton = rememberSaveable { mutableStateOf(false) }
NativeKeyboard(composeState, textStyle, showDeleteTextButton, userIsObserver, onMessageChange) NativeKeyboard(composeState, textStyle, showDeleteTextButton, userIsObserver, onMessageChange)
// Disable clicks on text field // Disable clicks on text field
if (cs.preview is ComposePreview.VoicePreview || !userCanSend || cs.inProgress) { if (cs.preview is ComposePreview.VoicePreview || !userCanSend) {
Box( Box(Modifier
Modifier .matchParentSize()
.matchParentSize() .clickable(enabled = !userCanSend, indication = null, interactionSource = remember { MutableInteractionSource() }, onClick = {
.clickable(enabled = !userCanSend, indication = null, interactionSource = remember { MutableInteractionSource() }, onClick = { AlertManager.shared.showAlertMsg(
AlertManager.shared.showAlertMsg( title = generalGetString(R.string.observer_cant_send_message_title),
title = generalGetString(MR.strings.observer_cant_send_message_title), text = generalGetString(R.string.observer_cant_send_message_desc)
text = generalGetString(MR.strings.observer_cant_send_message_desc) )
) })
})
) )
} }
if (showDeleteTextButton.value) { if (showDeleteTextButton.value) {
@@ -141,10 +125,9 @@ fun SendMsgView(
RecordVoiceView(recState, stopRecOnNextClick) RecordVoiceView(recState, stopRecOnNextClick)
} }
if (sendLiveMessage != null if (sendLiveMessage != null
&& updateLiveMessage != null && updateLiveMessage != null
&& (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(userCanSend) { StartLiveMessageButton(userCanSend) {
if (composeState.value.preview is ComposePreview.NoPreview) { if (composeState.value.preview is ComposePreview.NoPreview) {
@@ -161,55 +144,29 @@ fun SendMsgView(
} }
else -> { else -> {
val cs = composeState.value val cs = composeState.value
val icon = if (cs.editing || cs.liveMessage != null) painterResource(MR.images.ic_check_filled) else painterResource(MR.images.ic_arrow_upward) 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
val showDropdown = rememberSaveable { mutableStateOf(false) } if (cs.liveMessage == null &&
cs.preview !is ComposePreview.VoicePreview && !cs.editing &&
@Composable cs.contextItem is ComposeContextItem.NoContextItem &&
fun MenuItems(): List<@Composable () -> Unit> { sendLiveMessage != null && updateLiveMessage != null
val menuItems = mutableListOf<@Composable () -> Unit>() ) {
val showDropdown = rememberSaveable { mutableStateOf(false) }
if (cs.liveMessage == null && !cs.editing) {
if (
cs.preview !is ComposePreview.VoicePreview &&
cs.contextItem is ComposeContextItem.NoContextItem &&
sendLiveMessage != null && updateLiveMessage != null
) {
menuItems.add {
ItemAction(
generalGetString(MR.strings.send_live_message),
BoltFilled,
onClick = {
startLiveMessage(scope, sendLiveMessage, updateLiveMessage, sendButtonSize, sendButtonAlpha, composeState, liveMessageAlertShown)
showDropdown.value = false
}
)
}
}
if (timedMessageAllowed) {
menuItems.add {
ItemAction(
generalGetString(MR.strings.disappearing_message),
painterResource(MR.images.ic_timer),
onClick = {
showCustomDisappearingMessageDialog.value = true
showDropdown.value = false
}
)
}
}
}
return menuItems
}
val menuItems = MenuItems()
if (menuItems.isNotEmpty()) {
SendMsgButton(icon, sendButtonSize, sendButtonAlpha, !disabled, sendMessage) { showDropdown.value = true } SendMsgButton(icon, sendButtonSize, sendButtonAlpha, !disabled, sendMessage) { showDropdown.value = true }
DefaultDropdownMenu(showDropdown) {
menuItems.forEach { composable -> composable() } DefaultDropdownMenu(
showDropdown,
) {
ItemAction(
generalGetString(R.string.send_live_message),
BoltFilled,
onClick = {
startLiveMessage(scope, sendLiveMessage, updateLiveMessage, sendButtonSize, sendButtonAlpha, composeState, liveMessageAlertShown)
showDropdown.value = false
}
)
} }
} else { } else {
SendMsgButton(icon, sendButtonSize, sendButtonAlpha, !disabled, sendMessage) SendMsgButton(icon, sendButtonSize, sendButtonAlpha, !disabled, sendMessage)
@@ -220,99 +177,6 @@ fun SendMsgView(
} }
} }
@Composable
private fun CustomDisappearingMessageDialog(
sendMessage: (Int?) -> Unit,
setShowDialog: (Boolean) -> Unit,
customDisappearingMessageTimePref: SharedPreference<Int>?
) {
val showCustomTimePicker = remember { mutableStateOf(false) }
if (showCustomTimePicker.value) {
val selectedDisappearingMessageTime = remember {
mutableStateOf(customDisappearingMessageTimePref?.get?.invoke() ?: 300)
}
CustomTimePickerDialog(
selectedDisappearingMessageTime,
title = generalGetString(MR.strings.delete_after),
confirmButtonText = generalGetString(MR.strings.send_disappearing_message_send),
confirmButtonAction = { ttl ->
sendMessage(ttl)
customDisappearingMessageTimePref?.set?.invoke(ttl)
setShowDialog(false)
},
cancel = { setShowDialog(false) }
)
} else {
@Composable
fun ChoiceButton(
text: String,
onClick: () -> Unit
) {
TextButton(onClick) {
Text(
text,
fontSize = 18.sp,
color = MaterialTheme.colors.primary
)
}
}
Dialog(onDismissRequest = { setShowDialog(false) }) {
Surface(
shape = RoundedCornerShape(corner = CornerSize(25.dp))
) {
Box(
contentAlignment = Alignment.Center
) {
Column(
modifier = Modifier.padding(DEFAULT_PADDING),
verticalArrangement = Arrangement.spacedBy(6.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(" ") // centers title
Text(
generalGetString(MR.strings.send_disappearing_message),
fontSize = 16.sp,
color = MaterialTheme.colors.secondary
)
Icon(
painterResource(MR.images.ic_close),
generalGetString(MR.strings.icon_descr_close_button),
tint = MaterialTheme.colors.secondary,
modifier = Modifier
.size(25.dp)
.clickable { setShowDialog(false) }
)
}
ChoiceButton(generalGetString(MR.strings.send_disappearing_message_30_seconds)) {
sendMessage(30)
setShowDialog(false)
}
ChoiceButton(generalGetString(MR.strings.send_disappearing_message_1_minute)) {
sendMessage(60)
setShowDialog(false)
}
ChoiceButton(generalGetString(MR.strings.send_disappearing_message_5_minutes)) {
sendMessage(300)
setShowDialog(false)
}
ChoiceButton(generalGetString(MR.strings.send_disappearing_message_custom_time)) {
showCustomTimePicker.value = true
}
}
}
}
}
}
}
@Composable @Composable
private fun NativeKeyboard( private fun NativeKeyboard(
composeState: MutableState<ComposeState>, composeState: MutableState<ComposeState>,
@@ -386,13 +250,7 @@ private fun NativeKeyboard(
Log.e(chat.simplex.app.TAG, e.stackTraceToString()) Log.e(chat.simplex.app.TAG, e.stackTraceToString())
} }
} }
editText.doOnTextChanged { text, _, _, _ -> editText.doOnTextChanged { text, _, _, _ -> onMessageChange(text.toString()) }
if (!composeState.value.inProgress) {
onMessageChange(text.toString())
} else if (text.toString() != composeState.value.message) {
editText.setText(composeState.value.message)
}
}
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
}) { }) {
@@ -412,17 +270,17 @@ private fun NativeKeyboard(
imm.showSoftInput(it, InputMethodManager.SHOW_IMPLICIT) imm.showSoftInput(it, InputMethodManager.SHOW_IMPLICIT)
showKeyboard = false showKeyboard = false
} }
showDeleteTextButton.value = it.lineCount >= 4 && !cs.inProgress showDeleteTextButton.value = it.lineCount >= 4
} }
if (composeState.value.preview is ComposePreview.VoicePreview) { if (composeState.value.preview is ComposePreview.VoicePreview) {
ComposeOverlay(MR.strings.voice_message_send_text, textStyle, padding) ComposeOverlay(R.string.voice_message_send_text, textStyle, padding)
} else if (userIsObserver) { } else if (userIsObserver) {
ComposeOverlay(MR.strings.you_are_observer, textStyle, padding) ComposeOverlay(R.string.you_are_observer, textStyle, padding)
} }
} }
@Composable @Composable
private fun ComposeOverlay(textId: StringResource, textStyle: MutableState<TextStyle>, padding: PaddingValues) { private fun ComposeOverlay(textId: Int, textStyle: MutableState<TextStyle>, padding: PaddingValues) {
Text( Text(
generalGetString(textId), generalGetString(textId),
Modifier.padding(padding), Modifier.padding(padding),
@@ -437,13 +295,13 @@ private fun BoxScope.DeleteTextButton(composeState: MutableState<ComposeState>)
{ composeState.value = composeState.value.copy(message = "") }, { composeState.value = composeState.value.copy(message = "") },
Modifier.align(Alignment.TopEnd).size(36.dp) Modifier.align(Alignment.TopEnd).size(36.dp)
) { ) {
Icon(painterResource(MR.images.ic_close), null, Modifier.padding(7.dp).size(36.dp), tint = MaterialTheme.colors.secondary) Icon(painterResource(R.drawable.ic_close), null, Modifier.padding(7.dp).size(36.dp), tint = MaterialTheme.colors.secondary)
} }
} }
@Composable @Composable
private fun RecordVoiceView(recState: MutableState<RecordingState>, stopRecOnNextClick: MutableState<Boolean>) { private fun RecordVoiceView(recState: MutableState<RecordingState>, stopRecOnNextClick: MutableState<Boolean>) {
val rec: Recorder = remember { RecorderNative() } val rec: Recorder = remember { RecorderNative(MAX_VOICE_SIZE_FOR_SENDING) }
DisposableEffect(Unit) { onDispose { rec.stop() } } DisposableEffect(Unit) { onDispose { rec.stop() } }
val stopRecordingAndAddAudio: () -> Unit = { val stopRecordingAndAddAudio: () -> Unit = {
recState.value.filePathNullable?.let { recState.value.filePathNullable?.let {
@@ -494,8 +352,8 @@ private fun RecordVoiceView(recState: MutableState<RecordingState>, stopRecOnNex
private fun DisallowedVoiceButton(enabled: Boolean, onClick: () -> Unit) { private fun DisallowedVoiceButton(enabled: Boolean, onClick: () -> Unit) {
IconButton(onClick, Modifier.size(36.dp), enabled = enabled) { IconButton(onClick, Modifier.size(36.dp), enabled = enabled) {
Icon( Icon(
painterResource(MR.images.ic_keyboard_voice), painterResource(R.drawable.ic_keyboard_voice),
stringResource(MR.strings.icon_descr_record_voice_message), stringResource(R.string.icon_descr_record_voice_message),
tint = MaterialTheme.colors.secondary, tint = MaterialTheme.colors.secondary,
modifier = Modifier modifier = Modifier
.size(36.dp) .size(36.dp)
@@ -508,8 +366,8 @@ private fun DisallowedVoiceButton(enabled: Boolean, 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(
painterResource(MR.images.ic_keyboard_voice_filled), painterResource(R.drawable.ic_keyboard_voice_filled),
stringResource(MR.strings.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
.size(34.dp) .size(34.dp)
@@ -540,8 +398,8 @@ 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(
painterResource(MR.images.ic_stop_filled), painterResource(R.drawable.ic_stop_filled),
stringResource(MR.strings.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
.size(36.dp) .size(36.dp)
@@ -554,8 +412,8 @@ 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(
painterResource(MR.images.ic_keyboard_voice_filled), painterResource(R.drawable.ic_keyboard_voice_filled),
stringResource(MR.strings.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
.size(34.dp) .size(34.dp)
@@ -575,8 +433,8 @@ private fun CancelLiveMessageButton(
) { ) {
IconButton(onClick, Modifier.size(36.dp)) { IconButton(onClick, Modifier.size(36.dp)) {
Icon( Icon(
painterResource(MR.images.ic_close), painterResource(R.drawable.ic_close),
stringResource(MR.strings.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
.size(36.dp) .size(36.dp)
@@ -591,14 +449,14 @@ private fun SendMsgButton(
sizeDp: Animatable<Float, AnimationVector1D>, sizeDp: Animatable<Float, AnimationVector1D>,
alpha: Animatable<Float, AnimationVector1D>, alpha: Animatable<Float, AnimationVector1D>,
enabled: Boolean, enabled: Boolean,
sendMessage: (Int?) -> Unit, sendMessage: () -> Unit,
onLongClick: (() -> Unit)? = null onLongClick: (() -> Unit)? = null
) { ) {
val interactionSource = remember { MutableInteractionSource() } val interactionSource = remember { MutableInteractionSource() }
Box( Box(
modifier = Modifier.requiredSize(36.dp) modifier = Modifier.requiredSize(36.dp)
.combinedClickable( .combinedClickable(
onClick = { sendMessage(null) }, onClick = sendMessage,
onLongClick = onLongClick, onLongClick = onLongClick,
enabled = enabled, enabled = enabled,
role = Role.Button, role = Role.Button,
@@ -609,7 +467,7 @@ private fun SendMsgButton(
) { ) {
Icon( Icon(
icon, icon,
stringResource(MR.strings.icon_descr_send_message), stringResource(R.string.icon_descr_send_message),
tint = Color.White, tint = Color.White,
modifier = Modifier modifier = Modifier
.size(sizeDp.value.dp) .size(sizeDp.value.dp)
@@ -638,7 +496,7 @@ private fun StartLiveMessageButton(enabled: Boolean, onClick: () -> Unit) {
) { ) {
Icon( Icon(
BoltFilled, BoltFilled,
stringResource(MR.strings.icon_descr_send_message), stringResource(R.string.icon_descr_send_message),
tint = if (enabled) MaterialTheme.colors.primary else MaterialTheme.colors.secondary, tint = if (enabled) MaterialTheme.colors.primary else MaterialTheme.colors.secondary,
modifier = Modifier modifier = Modifier
.size(36.dp) .size(36.dp)
@@ -689,9 +547,9 @@ private fun startLiveMessage(
start() start()
} else { } else {
AlertManager.shared.showAlertDialog( AlertManager.shared.showAlertDialog(
title = generalGetString(MR.strings.live_message), title = generalGetString(R.string.live_message),
text = generalGetString(MR.strings.send_live_message_desc), text = generalGetString(R.string.send_live_message_desc),
confirmText = generalGetString(MR.strings.send_verb), confirmText = generalGetString(R.string.send_verb),
onConfirm = { onConfirm = {
liveMessageAlertShown.set(true) liveMessageAlertShown.set(true)
start() start()
@@ -701,22 +559,22 @@ private fun startLiveMessage(
private fun showNeedToAllowVoiceAlert(onConfirm: () -> Unit) { private fun showNeedToAllowVoiceAlert(onConfirm: () -> Unit) {
AlertManager.shared.showAlertDialog( AlertManager.shared.showAlertDialog(
title = generalGetString(MR.strings.allow_voice_messages_question), title = generalGetString(R.string.allow_voice_messages_question),
text = generalGetString(MR.strings.you_need_to_allow_to_send_voice), text = generalGetString(R.string.you_need_to_allow_to_send_voice),
confirmText = generalGetString(MR.strings.allow_verb), confirmText = generalGetString(R.string.allow_verb),
dismissText = generalGetString(MR.strings.cancel_verb), dismissText = generalGetString(R.string.cancel_verb),
onConfirm = onConfirm, onConfirm = onConfirm,
) )
} }
private fun showDisabledVoiceAlert(isDirectChat: Boolean) { private fun showDisabledVoiceAlert(isDirectChat: Boolean) {
AlertManager.shared.showAlertMsg( AlertManager.shared.showAlertMsg(
title = generalGetString(MR.strings.voice_messages_prohibited), title = generalGetString(R.string.voice_messages_prohibited),
text = generalGetString( text = generalGetString(
if (isDirectChat) if (isDirectChat)
MR.strings.ask_your_contact_to_enable_voice R.string.ask_your_contact_to_enable_voice
else else
MR.strings.only_group_owners_can_enable_voice R.string.only_group_owners_can_enable_voice
) )
) )
} }
@@ -743,7 +601,6 @@ fun PreviewSendMsgView() {
userIsObserver = false, userIsObserver = false,
userCanSend = true, userCanSend = true,
allowVoiceToContact = {}, allowVoiceToContact = {},
timedMessageAllowed = false,
sendMessage = {}, sendMessage = {},
onMessageChange = { _ -> }, onMessageChange = { _ -> },
textStyle = textStyle textStyle = textStyle
@@ -774,7 +631,6 @@ fun PreviewSendMsgViewEditing() {
userIsObserver = false, userIsObserver = false,
userCanSend = true, userCanSend = true,
allowVoiceToContact = {}, allowVoiceToContact = {},
timedMessageAllowed = false,
sendMessage = {}, sendMessage = {},
onMessageChange = { _ -> }, onMessageChange = { _ -> },
textStyle = textStyle textStyle = textStyle
@@ -805,7 +661,6 @@ fun PreviewSendMsgViewInProgress() {
userIsObserver = false, userIsObserver = false,
userCanSend = true, userCanSend = true,
allowVoiceToContact = {}, allowVoiceToContact = {},
timedMessageAllowed = false,
sendMessage = {}, sendMessage = {},
onMessageChange = { _ -> }, onMessageChange = { _ -> },
textStyle = textStyle textStyle = textStyle
@@ -10,8 +10,9 @@ 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 dev.icerock.moko.resources.compose.painterResource import androidx.compose.ui.platform.LocalContext
import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.res.painterResource
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
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
@@ -19,7 +20,6 @@ import chat.simplex.app.R
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
import chat.simplex.res.MR
@Composable @Composable
fun VerifyCodeView( fun VerifyCodeView(
@@ -61,14 +61,14 @@ private fun VerifyCodeLayout(
.verticalScroll(rememberScrollState()) .verticalScroll(rememberScrollState())
.padding(horizontal = DEFAULT_PADDING) .padding(horizontal = DEFAULT_PADDING)
) { ) {
AppBarTitle(stringResource(MR.strings.security_code), false) AppBarTitle(stringResource(R.string.security_code), false)
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(painterResource(MR.images.ic_verified_user), null, Modifier.padding(end = 4.dp).size(22.dp), tint = MaterialTheme.colors.secondary) Icon(painterResource(R.drawable.ic_verified_user), null, Modifier.padding(end = 4.dp).size(22.dp), tint = MaterialTheme.colors.secondary)
Text(String.format(stringResource(MR.strings.is_verified), displayName)) Text(String.format(stringResource(R.string.is_verified), displayName))
} else { } else {
Text(String.format(stringResource(MR.strings.is_not_verified), displayName)) Text(String.format(stringResource(R.string.is_not_verified), displayName))
} }
} }
@@ -86,16 +86,17 @@ private fun VerifyCodeLayout(
maxLines = 20 maxLines = 20
) )
} }
val context = LocalContext.current
Box(Modifier.weight(1f)) { Box(Modifier.weight(1f)) {
IconButton({ shareText(connectionCode) }, Modifier.size(20.dp).align(Alignment.CenterStart)) { IconButton({ shareText(context, connectionCode) }, Modifier.size(20.dp).align(Alignment.CenterStart)) {
Icon(painterResource(MR.images.ic_share_filled), 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))
} }
Text( Text(
generalGetString(MR.strings.to_verify_compare), generalGetString(R.string.to_verify_compare),
Modifier.padding(bottom = DEFAULT_PADDING) Modifier.padding(bottom = DEFAULT_PADDING)
) )
@@ -104,20 +105,20 @@ private fun VerifyCodeLayout(
horizontalArrangement = Arrangement.spacedBy(10.dp) horizontalArrangement = Arrangement.spacedBy(10.dp)
) { ) {
if (connectionVerified) { if (connectionVerified) {
SimpleButton(generalGetString(MR.strings.clear_verification), painterResource(MR.images.ic_shield)) { SimpleButton(generalGetString(R.string.clear_verification), painterResource(R.drawable.ic_shield)) {
verifyCode(null) {} verifyCode(null) {}
} }
} else { } else {
SimpleButton(generalGetString(MR.strings.scan_code), painterResource(MR.images.ic_qr_code)) { SimpleButton(generalGetString(R.string.scan_code), painterResource(R.drawable.ic_qr_code)) {
ModalManager.shared.showModal { ModalManager.shared.showModal {
ScanCodeView(verifyCode) { } ScanCodeView(verifyCode) { }
} }
} }
SimpleButton(generalGetString(MR.strings.mark_code_verified), painterResource(MR.images.ic_verified_user)) { 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(
title = generalGetString(MR.strings.incorrect_code) title = generalGetString(R.string.incorrect_code)
) )
} }
} }
@@ -11,43 +11,38 @@ 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.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.painter.Painter import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.platform.LocalView import androidx.compose.ui.res.painterResource
import dev.icerock.moko.resources.compose.painterResource import androidx.compose.ui.res.stringResource
import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.ui.text.input.TextFieldValue
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 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.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.newchat.InfoAboutIncognito
import chat.simplex.app.views.usersettings.SettingsActionItem import chat.simplex.app.views.usersettings.SettingsActionItem
import chat.simplex.res.MR
@Composable @Composable
fun AddGroupMembersView(groupInfo: GroupInfo, creatingGroup: Boolean = false, chatModel: ChatModel, close: () -> Unit) { fun AddGroupMembersView(groupInfo: GroupInfo, creatingGroup: Boolean = false, chatModel: ChatModel, close: () -> Unit) {
val selectedContacts = remember { mutableStateListOf<Long>() } val selectedContacts = remember { mutableStateListOf<Long>() }
val selectedRole = remember { mutableStateOf(GroupMemberRole.Member) } val selectedRole = remember { mutableStateOf(GroupMemberRole.Member) }
var allowModifyMembers by remember { mutableStateOf(true) } var allowModifyMembers by remember { mutableStateOf(true) }
val searchText = rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue("")) }
BackHandler(onBack = close) BackHandler(onBack = close)
AddGroupMembersLayout( AddGroupMembersLayout(
chatModel.incognito.value, chatModel.incognito.value,
groupInfo = groupInfo, groupInfo = groupInfo,
creatingGroup = creatingGroup, creatingGroup = creatingGroup,
contactsToAdd = getContactsToAdd(chatModel, searchText.value.text), contactsToAdd = getContactsToAdd(chatModel),
selectedContacts = selectedContacts, selectedContacts = selectedContacts,
selectedRole = selectedRole, selectedRole = selectedRole,
allowModifyMembers = allowModifyMembers, allowModifyMembers = allowModifyMembers,
searchText,
openPreferences = { openPreferences = {
ModalManager.shared.showCustomModal { close -> ModalManager.shared.showCustomModal { close ->
GroupPreferencesView(chatModel, groupInfo.id, close) GroupPreferencesView(chatModel, groupInfo.id, close)
@@ -74,8 +69,7 @@ fun AddGroupMembersView(groupInfo: GroupInfo, creatingGroup: Boolean = false, ch
) )
} }
fun getContactsToAdd(chatModel: ChatModel, search: String): List<Contact> { fun getContactsToAdd(chatModel: ChatModel): List<Contact> {
val s = search.trim().lowercase()
val memberContactIds = chatModel.groupMembers val memberContactIds = chatModel.groupMembers
.filter { it.memberCurrent } .filter { it.memberCurrent }
.mapNotNull { it.memberContactId } .mapNotNull { it.memberContactId }
@@ -84,7 +78,7 @@ fun getContactsToAdd(chatModel: ChatModel, search: String): List<Contact> {
.map { it.chatInfo } .map { it.chatInfo }
.filterIsInstance<ChatInfo.Direct>() .filterIsInstance<ChatInfo.Direct>()
.map { it.contact } .map { it.contact }
.filter { it.contactId !in memberContactIds && it.chatViewName.lowercase().contains(s) } .filter { it.contactId !in memberContactIds }
.sortedBy { it.displayName.lowercase() } .sortedBy { it.displayName.lowercase() }
.toList() .toList()
} }
@@ -98,7 +92,6 @@ fun AddGroupMembersLayout(
selectedContacts: List<Long>, selectedContacts: List<Long>,
selectedRole: MutableState<GroupMemberRole>, selectedRole: MutableState<GroupMemberRole>,
allowModifyMembers: Boolean, allowModifyMembers: Boolean,
searchText: MutableState<TextFieldValue>,
openPreferences: () -> Unit, openPreferences: () -> Unit,
inviteMembers: () -> Unit, inviteMembers: () -> Unit,
clearSelection: () -> Unit, clearSelection: () -> Unit,
@@ -111,12 +104,12 @@ fun AddGroupMembersLayout(
.fillMaxWidth() .fillMaxWidth()
.verticalScroll(rememberScrollState()), .verticalScroll(rememberScrollState()),
) { ) {
AppBarTitle(stringResource(MR.strings.button_add_members)) AppBarTitle(stringResource(R.string.button_add_members))
InfoAboutIncognito( InfoAboutIncognito(
chatModelIncognito, chatModelIncognito,
false, false,
generalGetString(MR.strings.group_unsupported_incognito_main_profile_sent), generalGetString(R.string.group_unsupported_incognito_main_profile_sent),
generalGetString(MR.strings.group_main_profile_sent), generalGetString(R.string.group_main_profile_sent),
true true
) )
Spacer(Modifier.size(DEFAULT_PADDING)) Spacer(Modifier.size(DEFAULT_PADDING))
@@ -132,13 +125,13 @@ fun AddGroupMembersLayout(
} }
SectionSpacer() SectionSpacer()
if (contactsToAdd.isEmpty() && searchText.value.text.isEmpty()) { if (contactsToAdd.isEmpty()) {
Row( Row(
Modifier.fillMaxWidth(), Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center horizontalArrangement = Arrangement.Center
) { ) {
Text( Text(
stringResource(MR.strings.no_contacts_to_add), stringResource(R.string.no_contacts_to_add),
Modifier.padding(), Modifier.padding(),
color = MaterialTheme.colors.secondary color = MaterialTheme.colors.secondary
) )
@@ -147,7 +140,7 @@ fun AddGroupMembersLayout(
SectionView { SectionView {
if (creatingGroup) { if (creatingGroup) {
SectionItemView(openPreferences) { SectionItemView(openPreferences) {
Text(stringResource(MR.strings.set_group_preferences)) Text(stringResource(R.string.set_group_preferences))
} }
} }
RoleSelectionRow(groupInfo, selectedRole, allowModifyMembers) RoleSelectionRow(groupInfo, selectedRole, allowModifyMembers)
@@ -161,10 +154,8 @@ fun AddGroupMembersLayout(
InviteSectionFooter(selectedContactsCount = selectedContacts.size, allowModifyMembers, clearSelection) InviteSectionFooter(selectedContactsCount = selectedContacts.size, allowModifyMembers, clearSelection)
} }
SectionDividerSpaced(maxTopPadding = true) SectionDividerSpaced(maxTopPadding = true)
SectionView(stringResource(MR.strings.select_contacts)) {
SectionItemView(padding = PaddingValues(start = DEFAULT_PADDING, end = DEFAULT_PADDING_HALF)) { SectionView(stringResource(R.string.select_contacts)) {
SearchRowView(searchText, selectedContacts.size)
}
ContactList(contacts = contactsToAdd, selectedContacts, groupInfo, allowModifyMembers, addContact, removeContact) ContactList(contacts = contactsToAdd, selectedContacts, groupInfo, allowModifyMembers, addContact, removeContact)
} }
} }
@@ -172,25 +163,6 @@ fun AddGroupMembersLayout(
} }
} }
@Composable
private fun SearchRowView(
searchText: MutableState<TextFieldValue> = rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue()) },
selectedContactsSize: Int
) {
Box(Modifier.width(36.dp), contentAlignment = Alignment.Center) {
Icon(painterResource(MR.images.ic_search), stringResource(MR.strings.search_verb), tint = MaterialTheme.colors.secondary)
}
Spacer(Modifier.width(DEFAULT_SPACE_AFTER_ICON))
SearchTextField(Modifier.fillMaxWidth(), searchText = searchText, alwaysVisible = true) {
searchText.value = searchText.value.copy(it)
}
val view = LocalView.current
LaunchedEffect(selectedContactsSize) {
searchText.value = searchText.value.copy("")
hideKeyboard(view)
}
}
@Composable @Composable
private fun RoleSelectionRow(groupInfo: GroupInfo, selectedRole: MutableState<GroupMemberRole>, enabled: Boolean) { private fun RoleSelectionRow(groupInfo: GroupInfo, selectedRole: MutableState<GroupMemberRole>, enabled: Boolean) {
Row( Row(
@@ -200,7 +172,7 @@ private fun RoleSelectionRow(groupInfo: GroupInfo, selectedRole: MutableState<Gr
) { ) {
val values = GroupMemberRole.values().filter { it <= groupInfo.membership.memberRole }.map { it to it.text } val values = GroupMemberRole.values().filter { it <= groupInfo.membership.memberRole }.map { it to it.text }
ExposedDropDownSettingRow( ExposedDropDownSettingRow(
generalGetString(MR.strings.new_member_role), generalGetString(R.string.new_member_role),
values, values,
selectedRole, selectedRole,
icon = null, icon = null,
@@ -212,8 +184,8 @@ private fun RoleSelectionRow(groupInfo: GroupInfo, selectedRole: MutableState<Gr
@Composable @Composable
fun InviteMembersButton(onClick: () -> Unit, disabled: Boolean) { fun InviteMembersButton(onClick: () -> Unit, disabled: Boolean) {
SettingsActionItem( SettingsActionItem(
painterResource(MR.images.ic_check), painterResource(R.drawable.ic_check),
stringResource(MR.strings.invite_to_group_button), stringResource(R.string.invite_to_group_button),
click = onClick, click = onClick,
textColor = MaterialTheme.colors.primary, textColor = MaterialTheme.colors.primary,
iconColor = MaterialTheme.colors.primary, iconColor = MaterialTheme.colors.primary,
@@ -224,8 +196,8 @@ fun InviteMembersButton(onClick: () -> Unit, disabled: Boolean) {
@Composable @Composable
fun SkipInvitingButton(onClick: () -> Unit) { fun SkipInvitingButton(onClick: () -> Unit) {
SettingsActionItem( SettingsActionItem(
painterResource(MR.images.ic_check), painterResource(R.drawable.ic_check),
stringResource(MR.strings.skip_inviting_button), stringResource(R.string.skip_inviting_button),
click = onClick, click = onClick,
textColor = MaterialTheme.colors.primary, textColor = MaterialTheme.colors.primary,
iconColor = MaterialTheme.colors.primary, iconColor = MaterialTheme.colors.primary,
@@ -241,7 +213,7 @@ fun InviteSectionFooter(selectedContactsCount: Int, enabled: Boolean, clearSelec
) { ) {
if (selectedContactsCount >= 1) { if (selectedContactsCount >= 1) {
Text( Text(
String.format(generalGetString(MR.strings.num_contacts_selected), selectedContactsCount), String.format(generalGetString(R.string.num_contacts_selected), selectedContactsCount),
color = MaterialTheme.colors.secondary, color = MaterialTheme.colors.secondary,
fontSize = 12.sp fontSize = 12.sp
) )
@@ -249,14 +221,14 @@ fun InviteSectionFooter(selectedContactsCount: Int, enabled: Boolean, clearSelec
Modifier.clickable { if (enabled) clearSelection() } Modifier.clickable { if (enabled) clearSelection() }
) { ) {
Text( Text(
stringResource(MR.strings.clear_contacts_selection_button), stringResource(R.string.clear_contacts_selection_button),
color = if (enabled) MaterialTheme.colors.primary else MaterialTheme.colors.secondary, color = if (enabled) MaterialTheme.colors.primary else MaterialTheme.colors.secondary,
fontSize = 12.sp fontSize = 12.sp
) )
} }
} else { } else {
Text( Text(
stringResource(MR.strings.no_contacts_selected), stringResource(R.string.no_contacts_selected),
color = MaterialTheme.colors.secondary, color = MaterialTheme.colors.secondary,
fontSize = 12.sp fontSize = 12.sp
) )
@@ -297,13 +269,13 @@ fun ContactCheckRow(
val icon: Painter val icon: Painter
val iconColor: Color val iconColor: Color
if (prohibitedToInviteIncognito) { if (prohibitedToInviteIncognito) {
icon = painterResource(MR.images.ic_theater_comedy_filled) icon = painterResource(R.drawable.ic_theater_comedy_filled)
iconColor = MaterialTheme.colors.secondary iconColor = MaterialTheme.colors.secondary
} else if (checked) { } else if (checked) {
icon = painterResource(MR.images.ic_check_circle_filled) icon = painterResource(R.drawable.ic_check_circle_filled)
iconColor = if (enabled) MaterialTheme.colors.primary else MaterialTheme.colors.secondary iconColor = if (enabled) MaterialTheme.colors.primary else MaterialTheme.colors.secondary
} else { } else {
icon = painterResource(MR.images.ic_circle) icon = painterResource(R.drawable.ic_circle)
iconColor = MaterialTheme.colors.secondary iconColor = MaterialTheme.colors.secondary
} }
SectionItemView( SectionItemView(
@@ -321,16 +293,13 @@ fun ContactCheckRow(
ProfileImage(size = 36.dp, contact.image) ProfileImage(size = 36.dp, contact.image)
Spacer(Modifier.width(DEFAULT_SPACE_AFTER_ICON)) Spacer(Modifier.width(DEFAULT_SPACE_AFTER_ICON))
Text( Text(
contact.chatViewName, contact.chatViewName, maxLines = 1, overflow = TextOverflow.Ellipsis,
modifier = Modifier.weight(10f, fill = true),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
color = if (prohibitedToInviteIncognito) MaterialTheme.colors.secondary else Color.Unspecified color = if (prohibitedToInviteIncognito) MaterialTheme.colors.secondary else Color.Unspecified
) )
Spacer(Modifier.fillMaxWidth().weight(1f)) Spacer(Modifier.fillMaxWidth().weight(1f))
Icon( Icon(
icon, icon,
contentDescription = stringResource(MR.strings.icon_descr_contact_checked), contentDescription = stringResource(R.string.icon_descr_contact_checked),
tint = iconColor tint = iconColor
) )
} }
@@ -338,9 +307,9 @@ fun ContactCheckRow(
fun showProhibitedToInviteIncognitoAlertDialog() { fun showProhibitedToInviteIncognitoAlertDialog() {
AlertManager.shared.showAlertMsg( AlertManager.shared.showAlertMsg(
title = generalGetString(MR.strings.invite_prohibited), title = generalGetString(R.string.invite_prohibited),
text = generalGetString(MR.strings.invite_prohibited_description), text = generalGetString(R.string.invite_prohibited_description),
confirmText = generalGetString(MR.strings.ok), confirmText = generalGetString(R.string.ok),
) )
} }
@@ -356,7 +325,6 @@ fun PreviewAddGroupMembersLayout() {
selectedContacts = remember { mutableStateListOf() }, selectedContacts = remember { mutableStateListOf() },
selectedRole = remember { mutableStateOf(GroupMemberRole.Admin) }, selectedRole = remember { mutableStateOf(GroupMemberRole.Admin) },
allowModifyMembers = true, allowModifyMembers = true,
searchText = remember { mutableStateOf(TextFieldValue("")) },
openPreferences = {}, openPreferences = {},
inviteMembers = {}, inviteMembers = {},
clearSelection = {}, clearSelection = {},
@@ -13,19 +13,17 @@ 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.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 dev.icerock.moko.resources.compose.painterResource import androidx.compose.ui.res.painterResource
import dev.icerock.moko.resources.compose.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.input.TextFieldValue
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 androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import chat.simplex.app.R
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.* import chat.simplex.app.ui.theme.*
@@ -34,7 +32,6 @@ import chat.simplex.app.views.chatlist.cantInviteIncognitoAlert
import chat.simplex.app.views.chatlist.setGroupMembers import chat.simplex.app.views.chatlist.setGroupMembers
import chat.simplex.app.views.helpers.* import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.usersettings.* import chat.simplex.app.views.usersettings.*
import chat.simplex.res.MR
@Composable @Composable
fun GroupChatInfoView(chatModel: ChatModel, groupLink: String?, groupLinkMemberRole: GroupMemberRole?, onGroupLinkUpdated: (Pair<String?, GroupMemberRole?>) -> Unit, close: () -> Unit) { fun GroupChatInfoView(chatModel: ChatModel, groupLink: String?, groupLinkMemberRole: GroupMemberRole?, onGroupLinkUpdated: (Pair<String?, GroupMemberRole?>) -> Unit, close: () -> Unit) {
@@ -61,8 +58,7 @@ fun GroupChatInfoView(chatModel: ChatModel, groupLink: String?, groupLinkMemberR
}, },
showMemberInfo = { member -> showMemberInfo = { member ->
withApi { withApi {
val r = chatModel.controller.apiGroupMemberInfo(groupInfo.groupId, member.groupMemberId) val stats = chatModel.controller.apiGroupMemberInfo(groupInfo.groupId, member.groupMemberId)
val stats = r?.second
val (_, code) = if (member.memberActive) { val (_, code) = if (member.memberActive) {
try { try {
chatModel.controller.apiGetGroupMemberCode(groupInfo.apiId, member.groupMemberId) chatModel.controller.apiGetGroupMemberCode(groupInfo.apiId, member.groupMemberId)
@@ -110,12 +106,12 @@ fun GroupChatInfoView(chatModel: ChatModel, groupLink: String?, groupLinkMemberR
fun deleteGroupDialog(chatInfo: ChatInfo, groupInfo: GroupInfo, chatModel: ChatModel, close: (() -> Unit)? = null) { fun deleteGroupDialog(chatInfo: ChatInfo, groupInfo: GroupInfo, chatModel: ChatModel, close: (() -> Unit)? = null) {
val alertTextKey = val alertTextKey =
if (groupInfo.membership.memberCurrent) MR.strings.delete_group_for_all_members_cannot_undo_warning if (groupInfo.membership.memberCurrent) R.string.delete_group_for_all_members_cannot_undo_warning
else MR.strings.delete_group_for_self_cannot_undo_warning else R.string.delete_group_for_self_cannot_undo_warning
AlertManager.shared.showAlertDialog( AlertManager.shared.showAlertDialog(
title = generalGetString(MR.strings.delete_group_question), title = generalGetString(R.string.delete_group_question),
text = generalGetString(alertTextKey), text = generalGetString(alertTextKey),
confirmText = generalGetString(MR.strings.delete_verb), confirmText = generalGetString(R.string.delete_verb),
onConfirm = { onConfirm = {
withApi { withApi {
val r = chatModel.controller.apiDeleteChat(chatInfo.chatType, chatInfo.apiId) val r = chatModel.controller.apiDeleteChat(chatInfo.chatType, chatInfo.apiId)
@@ -133,9 +129,9 @@ fun deleteGroupDialog(chatInfo: ChatInfo, groupInfo: GroupInfo, chatModel: ChatM
fun leaveGroupDialog(groupInfo: GroupInfo, chatModel: ChatModel, close: (() -> Unit)? = null) { fun leaveGroupDialog(groupInfo: GroupInfo, chatModel: ChatModel, close: (() -> Unit)? = null) {
AlertManager.shared.showAlertDialog( AlertManager.shared.showAlertDialog(
title = generalGetString(MR.strings.leave_group_question), title = generalGetString(R.string.leave_group_question),
text = generalGetString(MR.strings.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(MR.strings.leave_group_button), confirmText = generalGetString(R.string.leave_group_button),
onConfirm = { onConfirm = {
withApi { withApi {
chatModel.controller.leaveGroup(groupInfo.groupId) chatModel.controller.leaveGroup(groupInfo.groupId)
@@ -179,16 +175,14 @@ fun GroupChatInfoLayout(
SectionView { SectionView {
if (groupInfo.canEdit) { if (groupInfo.canEdit) {
EditGroupProfileButton(editGroupProfile) EditGroupProfileButton(editGroupProfile)
}
if (groupInfo.groupProfile.description != null || groupInfo.canEdit) {
AddOrEditWelcomeMessage(groupInfo.groupProfile.description, addOrEditWelcomeMessage) AddOrEditWelcomeMessage(groupInfo.groupProfile.description, addOrEditWelcomeMessage)
} }
GroupPreferencesButton(openPreferences) GroupPreferencesButton(openPreferences)
} }
SectionTextFooter(stringResource(MR.strings.only_group_owners_can_change_prefs)) SectionTextFooter(stringResource(R.string.only_group_owners_can_change_prefs))
SectionDividerSpaced(maxTopPadding = true) SectionDividerSpaced(maxTopPadding = true)
SectionView(title = String.format(generalGetString(MR.strings.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) {
if (groupLink == null) { if (groupLink == null) {
CreateGroupLinkButton(manageGroupLink) CreateGroupLinkButton(manageGroupLink)
@@ -200,17 +194,10 @@ fun GroupChatInfoLayout(
val tint = if (chat.chatInfo.incognito) MaterialTheme.colors.secondary else MaterialTheme.colors.primary val tint = if (chat.chatInfo.incognito) MaterialTheme.colors.secondary else MaterialTheme.colors.primary
AddMembersButton(tint, onAddMembersClick) AddMembersButton(tint, onAddMembersClick)
} }
val searchText = rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue()) }
val filteredMembers = remember { derivedStateOf { members.filter { it.chatViewName.lowercase().contains(searchText.value.text.trim()) } } }
if (members.size > 8) {
SectionItemView(padding = PaddingValues(start = 14.dp, end = DEFAULT_PADDING_HALF)) {
SearchRowView(searchText)
}
}
SectionItemView(minHeight = 54.dp) { SectionItemView(minHeight = 54.dp) {
MemberRow(groupInfo.membership, user = true) MemberRow(groupInfo.membership, user = true)
} }
MembersList(filteredMembers.value, showMemberInfo) MembersList(members, showMemberInfo)
} }
SectionDividerSpaced(maxTopPadding = true, maxBottomPadding = false) SectionDividerSpaced(maxTopPadding = true, maxBottomPadding = false)
SectionView { SectionView {
@@ -225,9 +212,9 @@ fun GroupChatInfoLayout(
if (developerTools) { if (developerTools) {
SectionDividerSpaced() SectionDividerSpaced()
SectionView(title = stringResource(MR.strings.section_title_for_console)) { SectionView(title = stringResource(R.string.section_title_for_console)) {
InfoRow(stringResource(MR.strings.info_row_local_name), groupInfo.localDisplayName) InfoRow(stringResource(R.string.info_row_local_name), groupInfo.localDisplayName)
InfoRow(stringResource(MR.strings.info_row_database_id), groupInfo.apiId.toString()) InfoRow(stringResource(R.string.info_row_database_id), groupInfo.apiId.toString())
} }
} }
SectionBottomSpacer() SectionBottomSpacer()
@@ -244,16 +231,14 @@ private fun GroupChatInfoHeader(cInfo: ChatInfo) {
Text( Text(
cInfo.displayName, style = MaterialTheme.typography.h1.copy(fontWeight = FontWeight.Normal), cInfo.displayName, style = MaterialTheme.typography.h1.copy(fontWeight = FontWeight.Normal),
color = MaterialTheme.colors.onBackground, color = MaterialTheme.colors.onBackground,
textAlign = TextAlign.Center, maxLines = 1,
maxLines = 4,
overflow = TextOverflow.Ellipsis overflow = TextOverflow.Ellipsis
) )
if (cInfo.fullName != "" && cInfo.fullName != cInfo.displayName) { if (cInfo.fullName != "" && cInfo.fullName != cInfo.displayName) {
Text( Text(
cInfo.fullName, style = MaterialTheme.typography.h2, cInfo.fullName, style = MaterialTheme.typography.h2,
color = MaterialTheme.colors.onBackground, color = MaterialTheme.colors.onBackground,
textAlign = TextAlign.Center, maxLines = 2,
maxLines = 8,
overflow = TextOverflow.Ellipsis overflow = TextOverflow.Ellipsis
) )
} }
@@ -263,8 +248,8 @@ private fun GroupChatInfoHeader(cInfo: ChatInfo) {
@Composable @Composable
private fun GroupPreferencesButton(onClick: () -> Unit) { private fun GroupPreferencesButton(onClick: () -> Unit) {
SettingsActionItem( SettingsActionItem(
painterResource(MR.images.ic_toggle_on), painterResource(R.drawable.ic_toggle_on),
stringResource(MR.strings.group_preferences), stringResource(R.string.group_preferences),
click = onClick click = onClick
) )
} }
@@ -272,8 +257,8 @@ private fun GroupPreferencesButton(onClick: () -> Unit) {
@Composable @Composable
private fun AddMembersButton(tint: Color = MaterialTheme.colors.primary, onClick: () -> Unit) { private fun AddMembersButton(tint: Color = MaterialTheme.colors.primary, onClick: () -> Unit) {
SettingsActionItem( SettingsActionItem(
painterResource(MR.images.ic_add), painterResource(R.drawable.ic_add),
stringResource(MR.strings.button_add_members), stringResource(R.string.button_add_members),
onClick, onClick,
iconColor = tint, iconColor = tint,
textColor = tint textColor = tint
@@ -317,7 +302,7 @@ private fun MemberRow(member: GroupMember, user: Boolean = false) {
) )
} }
val s = member.memberStatus.shortText val s = member.memberStatus.shortText
val statusDescr = if (user) String.format(generalGetString(MR.strings.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 = MaterialTheme.colors.secondary, color = MaterialTheme.colors.secondary,
@@ -336,14 +321,14 @@ private fun MemberRow(member: GroupMember, user: Boolean = false) {
@Composable @Composable
private fun MemberVerifiedShield() { private fun MemberVerifiedShield() {
Icon(painterResource(MR.images.ic_verified_user), null, Modifier.padding(end = 3.dp).size(16.dp), tint = MaterialTheme.colors.secondary) 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(onClick: () -> Unit) { private fun GroupLinkButton(onClick: () -> Unit) {
SettingsActionItem( SettingsActionItem(
painterResource(MR.images.ic_link), painterResource(R.drawable.ic_link),
stringResource(MR.strings.group_link), stringResource(R.string.group_link),
onClick, onClick,
iconColor = MaterialTheme.colors.secondary iconColor = MaterialTheme.colors.secondary
) )
@@ -352,8 +337,8 @@ private fun GroupLinkButton(onClick: () -> Unit) {
@Composable @Composable
private fun CreateGroupLinkButton(onClick: () -> Unit) { private fun CreateGroupLinkButton(onClick: () -> Unit) {
SettingsActionItem( SettingsActionItem(
painterResource(MR.images.ic_add_link), painterResource(R.drawable.ic_add_link),
stringResource(MR.strings.create_group_link), stringResource(R.string.create_group_link),
onClick, onClick,
iconColor = MaterialTheme.colors.secondary iconColor = MaterialTheme.colors.secondary
) )
@@ -362,8 +347,8 @@ private fun CreateGroupLinkButton(onClick: () -> Unit) {
@Composable @Composable
fun EditGroupProfileButton(onClick: () -> Unit) { fun EditGroupProfileButton(onClick: () -> Unit) {
SettingsActionItem( SettingsActionItem(
painterResource(MR.images.ic_edit), painterResource(R.drawable.ic_edit),
stringResource(MR.strings.button_edit_group_profile), stringResource(R.string.button_edit_group_profile),
onClick, onClick,
iconColor = MaterialTheme.colors.secondary iconColor = MaterialTheme.colors.secondary
) )
@@ -372,12 +357,12 @@ fun EditGroupProfileButton(onClick: () -> Unit) {
@Composable @Composable
private fun AddOrEditWelcomeMessage(welcomeMessage: String?, onClick: () -> Unit) { private fun AddOrEditWelcomeMessage(welcomeMessage: String?, onClick: () -> Unit) {
val text = if (welcomeMessage == null) { val text = if (welcomeMessage == null) {
stringResource(MR.strings.button_add_welcome_message) stringResource(R.string.button_add_welcome_message)
} else { } else {
stringResource(MR.strings.button_welcome_message) stringResource(R.string.button_welcome_message)
} }
SettingsActionItem( SettingsActionItem(
painterResource(MR.images.ic_maps_ugc), painterResource(R.drawable.ic_maps_ugc),
text, text,
onClick, onClick,
iconColor = MaterialTheme.colors.secondary iconColor = MaterialTheme.colors.secondary
@@ -387,8 +372,8 @@ private fun AddOrEditWelcomeMessage(welcomeMessage: String?, onClick: () -> Unit
@Composable @Composable
private fun LeaveGroupButton(onClick: () -> Unit) { private fun LeaveGroupButton(onClick: () -> Unit) {
SettingsActionItem( SettingsActionItem(
painterResource(MR.images.ic_logout), painterResource(R.drawable.ic_logout),
stringResource(MR.strings.button_leave_group), stringResource(R.string.button_leave_group),
onClick, onClick,
iconColor = Color.Red, iconColor = Color.Red,
textColor = Color.Red textColor = Color.Red
@@ -398,27 +383,14 @@ private fun LeaveGroupButton(onClick: () -> Unit) {
@Composable @Composable
private fun DeleteGroupButton(onClick: () -> Unit) { private fun DeleteGroupButton(onClick: () -> Unit) {
SettingsActionItem( SettingsActionItem(
painterResource(MR.images.ic_delete), painterResource(R.drawable.ic_delete),
stringResource(MR.strings.button_delete_group), stringResource(R.string.button_delete_group),
onClick, onClick,
iconColor = Color.Red, iconColor = Color.Red,
textColor = Color.Red textColor = Color.Red
) )
} }
@Composable
private fun SearchRowView(
searchText: MutableState<TextFieldValue> = rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue()) }
) {
Box(Modifier.width(36.dp), contentAlignment = Alignment.Center) {
Icon(painterResource(MR.images.ic_search), stringResource(MR.strings.search_verb), tint = MaterialTheme.colors.secondary)
}
Spacer(Modifier.width(14.dp))
SearchTextField(Modifier.fillMaxWidth(), searchText = searchText, alwaysVisible = true) {
searchText.value = searchText.value.copy(it)
}
}
@Preview @Preview
@Composable @Composable
fun PreviewGroupChatInfoLayout() { fun PreviewGroupChatInfoLayout() {
@@ -10,8 +10,9 @@ 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 dev.icerock.moko.resources.compose.painterResource import androidx.compose.ui.platform.LocalContext
import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.res.painterResource
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
@@ -19,13 +20,13 @@ 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 chat.simplex.app.views.newchat.QRCode import chat.simplex.app.views.newchat.QRCode
import chat.simplex.res.MR
@Composable @Composable
fun GroupLinkView(chatModel: ChatModel, groupInfo: GroupInfo, connReqContact: String?, memberRole: GroupMemberRole?, onGroupLinkUpdated: (Pair<String?, GroupMemberRole?>) -> 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) } val groupLinkMemberRole = rememberSaveable { mutableStateOf(memberRole) }
var creatingLink by rememberSaveable { mutableStateOf(false) } var creatingLink by rememberSaveable { mutableStateOf(false) }
val cxt = LocalContext.current
fun createLink() { fun createLink() {
creatingLink = true creatingLink = true
withApi { withApi {
@@ -49,7 +50,7 @@ fun GroupLinkView(chatModel: ChatModel, groupInfo: GroupInfo, connReqContact: St
groupLinkMemberRole, groupLinkMemberRole,
creatingLink, creatingLink,
createLink = ::createLink, createLink = ::createLink,
share = { shareText(groupLink ?: return@GroupLinkLayout) }, share = { shareText(cxt, groupLink ?: return@GroupLinkLayout) },
updateLink = { updateLink = {
val role = groupLinkMemberRole.value val role = groupLinkMemberRole.value
if (role != null) { if (role != null) {
@@ -65,9 +66,9 @@ fun GroupLinkView(chatModel: ChatModel, groupInfo: GroupInfo, connReqContact: St
}, },
deleteLink = { deleteLink = {
AlertManager.shared.showAlertDialog( AlertManager.shared.showAlertDialog(
title = generalGetString(MR.strings.delete_link_question), title = generalGetString(R.string.delete_link_question),
text = generalGetString(MR.strings.all_group_members_will_remain_connected), text = generalGetString(R.string.all_group_members_will_remain_connected),
confirmText = generalGetString(MR.strings.delete_verb), confirmText = generalGetString(R.string.delete_verb),
onConfirm = { onConfirm = {
withApi { withApi {
val r = chatModel.controller.apiDeleteGroupLink(groupInfo.groupId) val r = chatModel.controller.apiDeleteGroupLink(groupInfo.groupId)
@@ -101,9 +102,9 @@ fun GroupLinkLayout(
Modifier Modifier
.verticalScroll(rememberScrollState()), .verticalScroll(rememberScrollState()),
) { ) {
AppBarTitle(stringResource(MR.strings.group_link)) AppBarTitle(stringResource(R.string.group_link))
Text( Text(
stringResource(MR.strings.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(start = DEFAULT_PADDING, end = DEFAULT_PADDING, bottom = 12.dp), Modifier.padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, bottom = 12.dp),
lineHeight = 22.sp lineHeight = 22.sp
) )
@@ -113,7 +114,7 @@ fun GroupLinkLayout(
verticalArrangement = Arrangement.SpaceEvenly verticalArrangement = Arrangement.SpaceEvenly
) { ) {
if (groupLink == null) { if (groupLink == null) {
SimpleButton(stringResource(MR.strings.button_create_group_link), icon = painterResource(MR.images.ic_add_link), 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 {
RoleSelectionRow(groupInfo, groupLinkMemberRole) RoleSelectionRow(groupInfo, groupLinkMemberRole)
var initialLaunch by remember { mutableStateOf(true) } var initialLaunch by remember { mutableStateOf(true) }
@@ -130,13 +131,13 @@ fun GroupLinkLayout(
modifier = Modifier.padding(horizontal = DEFAULT_PADDING, vertical = 10.dp) modifier = Modifier.padding(horizontal = DEFAULT_PADDING, vertical = 10.dp)
) { ) {
SimpleButton( SimpleButton(
stringResource(MR.strings.share_link), stringResource(R.string.share_link),
icon = painterResource(MR.images.ic_share), icon = painterResource(R.drawable.ic_share),
click = share click = share
) )
SimpleButton( SimpleButton(
stringResource(MR.strings.delete_link), stringResource(R.string.delete_link),
icon = painterResource(MR.images.ic_delete), icon = painterResource(R.drawable.ic_delete),
color = Color.Red, color = Color.Red,
click = deleteLink click = deleteLink
) )
@@ -156,7 +157,7 @@ private fun RoleSelectionRow(groupInfo: GroupInfo, selectedRole: MutableState<Gr
) { ) {
val values = listOf(GroupMemberRole.Member, GroupMemberRole.Observer).map { it to it.text } val values = listOf(GroupMemberRole.Member, GroupMemberRole.Observer).map { it to it.text }
ExposedDropDownSettingRow( ExposedDropDownSettingRow(
generalGetString(MR.strings.initial_member_role), generalGetString(R.string.initial_member_role),
values, values,
selectedRole, selectedRole,
icon = null, icon = null,
@@ -0,0 +1,349 @@
package chat.simplex.app.views.chat.group
import InfoRow
import SectionBottomSpacer
import SectionDividerSpaced
import SectionSpacer
import SectionView
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
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.chat.*
import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.usersettings.SettingsActionItem
import kotlinx.datetime.Clock
@Composable
fun GroupMemberInfoView(
groupInfo: GroupInfo,
member: GroupMember,
connStats: ConnectionStats?,
connectionCode: String?,
chatModel: ChatModel,
close: () -> Unit,
closeAll: () -> Unit, // Close all open windows up to ChatView
) {
BackHandler(onBack = close)
val chat = chatModel.chats.firstOrNull { it.id == chatModel.chatId.value }
val developerTools = chatModel.controller.appPrefs.developerTools.get()
if (chat != null) {
val newRole = remember { mutableStateOf(member.memberRole) }
GroupMemberInfoLayout(
groupInfo,
member,
connStats,
newRole,
developerTools,
connectionCode,
getContactChat = { chatModel.getContactChat(it) },
openDirectChat = {
withApi {
val c = chatModel.controller.apiGetChat(ChatType.Direct, it)
if (c != null) {
if (chatModel.getContactChat(it) == null) {
chatModel.addChat(c)
}
chatModel.chatItems.clear()
chatModel.chatItems.addAll(c.chatItems)
chatModel.chatId.value = c.id
closeAll()
}
}
},
removeMember = { removeMemberDialog(groupInfo, member, chatModel, close) },
onRoleSelected = {
if (it == newRole.value) return@GroupMemberInfoLayout
val prevValue = newRole.value
newRole.value = it
updateMemberRoleDialog(it, member, onDismiss = {
newRole.value = prevValue
}) {
withApi {
kotlin.runCatching {
val mem = chatModel.controller.apiMemberRole(groupInfo.groupId, member.groupMemberId, it)
chatModel.upsertGroupMember(groupInfo, mem)
}.onFailure {
newRole.value = prevValue
}
}
}
},
switchMemberAddress = {
switchMemberAddress(chatModel, groupInfo, member)
},
verifyClicked = {
ModalManager.shared.showModalCloseable { close ->
remember { derivedStateOf { chatModel.groupMembers.firstOrNull { it.memberId == member.memberId } } }.value?.let { mem ->
VerifyCodeView(
mem.displayName,
connectionCode,
mem.verified,
verify = { code ->
chatModel.controller.apiVerifyGroupMember(mem.groupId, mem.groupMemberId, code)?.let { r ->
val (verified, existingCode) = r
chatModel.upsertGroupMember(
groupInfo,
mem.copy(
activeConn = mem.activeConn?.copy(
connectionCode = if (verified) SecurityCode(existingCode, Clock.System.now()) else null
)
)
)
r
}
},
close,
)
}
}
}
)
}
}
fun removeMemberDialog(groupInfo: GroupInfo, member: GroupMember, chatModel: ChatModel, close: (() -> Unit)? = null) {
AlertManager.shared.showAlertDialog(
title = generalGetString(R.string.button_remove_member),
text = generalGetString(R.string.member_will_be_removed_from_group_cannot_be_undone),
confirmText = generalGetString(R.string.remove_member_confirmation),
onConfirm = {
withApi {
val removedMember = chatModel.controller.apiRemoveMember(member.groupId, member.groupMemberId)
if (removedMember != null) {
chatModel.upsertGroupMember(groupInfo, removedMember)
}
close?.invoke()
}
},
destructive = true,
)
}
@Composable
fun GroupMemberInfoLayout(
groupInfo: GroupInfo,
member: GroupMember,
connStats: ConnectionStats?,
newRole: MutableState<GroupMemberRole>,
developerTools: Boolean,
connectionCode: String?,
getContactChat: (Long) -> Chat?,
openDirectChat: (Long) -> Unit,
removeMember: () -> Unit,
onRoleSelected: (GroupMemberRole) -> Unit,
switchMemberAddress: () -> Unit,
verifyClicked: () -> Unit,
) {
Column(
Modifier
.fillMaxWidth()
.verticalScroll(rememberScrollState()),
) {
Row(
Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center
) {
GroupMemberInfoHeader(member)
}
SectionSpacer()
if (member.memberActive) {
val contactId = member.memberContactId
if (contactId != null) {
SectionView {
val chat = getContactChat(contactId)
if ((chat != null && chat.chatInfo is ChatInfo.Direct && chat.chatInfo.contact.directOrUsed) || groupInfo.fullGroupPreferences.directMessages.on) {
OpenChatButton(onClick = { openDirectChat(contactId) })
}
if (connectionCode != null) {
VerifyCodeButton(member.verified, verifyClicked)
}
}
SectionSpacer()
}
}
SectionView(title = stringResource(R.string.member_info_section_title_member)) {
InfoRow(stringResource(R.string.info_row_group), groupInfo.displayName)
val roles = remember { member.canChangeRoleTo(groupInfo) }
if (roles != null) {
RoleSelectionRow(roles, newRole, onRoleSelected)
} else {
InfoRow(stringResource(R.string.role_in_group), member.memberRole.text)
}
val conn = member.activeConn
if (conn != null) {
val connLevelDesc =
if (conn.connLevel == 0) stringResource(R.string.conn_level_desc_direct)
else String.format(generalGetString(R.string.conn_level_desc_indirect), conn.connLevel)
InfoRow(stringResource(R.string.info_row_connection), connLevelDesc)
}
}
if (connStats != null) {
SectionDividerSpaced()
SectionView(title = stringResource(R.string.conn_stats_section_title_servers)) {
SwitchAddressButton(switchMemberAddress)
val rcvServers = connStats.rcvServers
val sndServers = connStats.sndServers
if ((rcvServers != null && rcvServers.isNotEmpty()) || (sndServers != null && sndServers.isNotEmpty())) {
if (rcvServers != null && rcvServers.isNotEmpty()) {
SimplexServers(stringResource(R.string.receiving_via), rcvServers)
if (sndServers != null && sndServers.isNotEmpty()) {
SimplexServers(stringResource(R.string.sending_via), sndServers)
}
} else if (sndServers != null && sndServers.isNotEmpty()) {
SimplexServers(stringResource(R.string.sending_via), sndServers)
}
}
}
}
if (member.canBeRemoved(groupInfo)) {
SectionDividerSpaced(maxBottomPadding = false)
SectionView {
RemoveMemberButton(removeMember)
}
}
if (developerTools) {
SectionDividerSpaced()
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_database_id), member.groupMemberId.toString())
}
}
SectionBottomSpacer()
}
}
@Composable
fun GroupMemberInfoHeader(member: GroupMember) {
Column(
Modifier.padding(horizontal = 8.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
ProfileImage(size = 192.dp, member.image, color = if (isInDarkTheme()) GroupDark else SettingsSecondaryLight)
Row(verticalAlignment = Alignment.CenterVertically) {
if (member.verified) {
Icon(painterResource(R.drawable.ic_verified_user), null, Modifier.padding(end = 6.dp, top = 4.dp).size(24.dp), tint = MaterialTheme.colors.secondary)
}
Text(
member.displayName, style = MaterialTheme.typography.h1.copy(fontWeight = FontWeight.Normal),
color = MaterialTheme.colors.onBackground,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
if (member.fullName != "" && member.fullName != member.displayName) {
Text(
member.fullName, style = MaterialTheme.typography.h2,
color = MaterialTheme.colors.onBackground,
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
}
}
}
@Composable
fun RemoveMemberButton(onClick: () -> Unit) {
SettingsActionItem(
painterResource(R.drawable.ic_delete),
stringResource(R.string.button_remove_member),
click = onClick,
textColor = Color.Red,
iconColor = Color.Red,
)
}
@Composable
fun OpenChatButton(onClick: () -> Unit) {
SettingsActionItem(
painterResource(R.drawable.ic_chat),
stringResource(R.string.button_send_direct_message),
click = onClick,
textColor = MaterialTheme.colors.primary,
iconColor = MaterialTheme.colors.primary,
)
}
@Composable
private fun RoleSelectionRow(
roles: List<GroupMemberRole>,
selectedRole: MutableState<GroupMemberRole>,
onSelected: (GroupMemberRole) -> Unit
) {
Row(
Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
val values = remember { roles.map { it to it.text } }
ExposedDropDownSettingRow(
generalGetString(R.string.change_role),
values,
selectedRole,
icon = null,
enabled = remember { mutableStateOf(true) },
onSelected = onSelected
)
}
}
private fun updateMemberRoleDialog(
newRole: GroupMemberRole,
member: GroupMember,
onDismiss: () -> Unit,
onConfirm: () -> Unit
) {
AlertManager.shared.showAlertDialog(
title = generalGetString(R.string.change_member_role_question),
text = if (member.memberCurrent)
String.format(generalGetString(R.string.member_role_will_be_changed_with_notification), newRole.text)
else
String.format(generalGetString(R.string.member_role_will_be_changed_with_invitation), newRole.text),
confirmText = generalGetString(R.string.change_verb),
onDismiss = onDismiss,
onConfirm = onConfirm,
onDismissRequest = onDismiss
)
}
private fun switchMemberAddress(m: ChatModel, groupInfo: GroupInfo, member: GroupMember) = withApi {
m.controller.apiSwitchGroupMember(groupInfo.apiId, member.groupMemberId)
}
@Preview
@Composable
fun PreviewGroupMemberInfoLayout() {
SimpleXTheme {
GroupMemberInfoLayout(
groupInfo = GroupInfo.sampleData,
member = GroupMember.sampleData,
connStats = null,
newRole = remember { mutableStateOf(GroupMemberRole.Member) },
developerTools = false,
connectionCode = "123",
getContactChat = { Chat.sampleData },
openDirectChat = {},
removeMember = {},
onRoleSelected = {},
switchMemberAddress = {},
verifyClicked = {},
)
}
}
@@ -13,13 +13,13 @@ 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.Modifier import androidx.compose.ui.Modifier
import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.res.stringResource
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.TimedMessagesTTLPicker
import chat.simplex.app.views.helpers.* import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.usersettings.PreferenceToggleWithIcon import chat.simplex.app.views.usersettings.PreferenceToggleWithIcon
import chat.simplex.res.MR
@Composable @Composable
fun GroupPreferencesView(m: ChatModel, chatId: String, close: () -> Unit,) { fun GroupPreferencesView(m: ChatModel, chatId: String, close: () -> Unit,) {
@@ -31,9 +31,9 @@ fun GroupPreferencesView(m: ChatModel, chatId: String, close: () -> Unit,) {
fun savePrefs(afterSave: () -> Unit = {}) { fun savePrefs(afterSave: () -> Unit = {}) {
withApi { withApi {
val gp = gInfo.groupProfile.copy(groupPreferences = preferences.toGroupPreferences()) val gp = gInfo.groupProfile.copy(groupPreferences = preferences.toGroupPreferences())
val g = m.controller.apiUpdateGroup(gInfo.groupId, gp) val gInfo = m.controller.apiUpdateGroup(gInfo.groupId, gp)
if (g != null) { if (gInfo != null) {
m.updateGroup(g) m.updateGroup(gInfo)
currentPreferences = preferences currentPreferences = preferences
} }
afterSave() afterSave()
@@ -72,14 +72,14 @@ private fun GroupPreferencesLayout(
Column( Column(
Modifier.fillMaxWidth().verticalScroll(rememberScrollState()), Modifier.fillMaxWidth().verticalScroll(rememberScrollState()),
) { ) {
AppBarTitle(stringResource(MR.strings.group_preferences)) AppBarTitle(stringResource(R.string.group_preferences))
val timedMessages = remember(preferences) { mutableStateOf(preferences.timedMessages.enable) } val timedMessages = remember(preferences) { mutableStateOf(preferences.timedMessages.enable) }
val onTTLUpdated = { ttl: Int? -> val onTTLUpdated = { ttl: Int? ->
applyPrefs(preferences.copy(timedMessages = preferences.timedMessages.copy(ttl = ttl))) applyPrefs(preferences.copy(timedMessages = preferences.timedMessages.copy(ttl = ttl ?: 86400)))
} }
FeatureSection(GroupFeature.TimedMessages, timedMessages, groupInfo, preferences, onTTLUpdated) { enable -> FeatureSection(GroupFeature.TimedMessages, timedMessages, groupInfo, preferences, onTTLUpdated) { enable ->
if (enable == GroupFeatureEnabled.ON) { if (enable == GroupFeatureEnabled.ON) {
applyPrefs(preferences.copy(timedMessages = TimedMessagesGroupPreference(enable = enable, ttl = preferences.timedMessages.ttl ?: 86400))) applyPrefs(preferences.copy(timedMessages = TimedMessagesGroupPreference(enable = enable, ttl = preferences.timedMessages.ttl ?: 86400)))
} else { } else {
applyPrefs(preferences.copy(timedMessages = TimedMessagesGroupPreference(enable = enable, ttl = currentPreferences.timedMessages.ttl))) applyPrefs(preferences.copy(timedMessages = TimedMessagesGroupPreference(enable = enable, ttl = currentPreferences.timedMessages.ttl)))
} }
@@ -95,21 +95,10 @@ private fun GroupPreferencesLayout(
applyPrefs(preferences.copy(fullDelete = GroupPreference(enable = it))) applyPrefs(preferences.copy(fullDelete = GroupPreference(enable = it)))
} }
SectionDividerSpaced(true, maxBottomPadding = false) SectionDividerSpaced(true, maxBottomPadding = false)
val allowReactions = remember(preferences) { mutableStateOf(preferences.reactions.enable) }
FeatureSection(GroupFeature.Reactions, allowReactions, groupInfo, preferences, onTTLUpdated) {
applyPrefs(preferences.copy(reactions = GroupPreference(enable = it)))
}
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)))
} }
// TODO uncomment in 5.3
// SectionDividerSpaced(true, maxBottomPadding = false)
// val allowFiles = remember(preferences) { mutableStateOf(preferences.files.enable) }
// FeatureSection(GroupFeature.Files, allowFiles, groupInfo, preferences, onTTLUpdated) {
// applyPrefs(preferences.copy(files = GroupPreference(enable = it)))
// }
if (groupInfo.canEdit) { if (groupInfo.canEdit) {
SectionDividerSpaced(maxTopPadding = true, maxBottomPadding = false) SectionDividerSpaced(maxTopPadding = true, maxBottomPadding = false)
ResetSaveButtons( ResetSaveButtons(
@@ -147,15 +136,7 @@ private fun FeatureSection(
} }
if (timedOn) { if (timedOn) {
val ttl = rememberSaveable(preferences.timedMessages) { mutableStateOf(preferences.timedMessages.ttl) } val ttl = rememberSaveable(preferences.timedMessages) { mutableStateOf(preferences.timedMessages.ttl) }
DropdownCustomTimePickerSettingRow( TimedMessagesTTLPicker(ttl, onTTLUpdated)
selection = ttl,
propagateExternalSelectionUpdate = true, // for Reset
label = generalGetString(MR.strings.delete_after),
dropdownValues = TimedMessagesPreference.ttlValues,
customPickerTitle = generalGetString(MR.strings.delete_after),
customPickerConfirmButtonText = generalGetString(MR.strings.custom_time_picker_select),
onSelected = onTTLUpdated
)
} }
} else { } else {
InfoRow( InfoRow(
@@ -165,7 +146,7 @@ private fun FeatureSection(
iconTint = iconTint, iconTint = iconTint,
) )
if (timedOn) { if (timedOn) {
InfoRow(generalGetString(MR.strings.delete_after), timeText(preferences.timedMessages.ttl)) InfoRow(generalGetString(R.string.delete_after), TimedMessagesPreference.ttlText(preferences.timedMessages.ttl))
} }
} }
} }
@@ -176,19 +157,19 @@ 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(MR.strings.reset_verb), color = if (disabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary) Text(stringResource(R.string.reset_verb), color = if (disabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary)
} }
SectionItemView(save, disabled = disabled) { SectionItemView(save, disabled = disabled) {
Text(stringResource(MR.strings.save_and_notify_group_members), color = if (disabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary) Text(stringResource(R.string.save_and_notify_group_members), color = if (disabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary)
} }
} }
} }
private fun showUnsavedChangesAlert(save: () -> Unit, revert: () -> Unit) { private fun showUnsavedChangesAlert(save: () -> Unit, revert: () -> Unit) {
AlertManager.shared.showAlertDialogStacked( AlertManager.shared.showAlertDialogStacked(
title = generalGetString(MR.strings.save_preferences_question), title = generalGetString(R.string.save_preferences_question),
confirmText = generalGetString(MR.strings.save_and_notify_group_members), confirmText = generalGetString(R.string.save_and_notify_group_members),
dismissText = generalGetString(MR.strings.exit_without_saving), dismissText = generalGetString(R.string.exit_without_saving),
onConfirm = save, onConfirm = save,
onDismiss = revert, onDismiss = revert,
) )
@@ -13,7 +13,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier 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 dev.icerock.moko.resources.compose.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
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@@ -28,7 +28,6 @@ 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
import chat.simplex.res.MR
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -106,7 +105,7 @@ fun GroupProfileLayout(
Modifier.fillMaxWidth() Modifier.fillMaxWidth()
.padding(horizontal = DEFAULT_PADDING) .padding(horizontal = DEFAULT_PADDING)
) { ) {
ReadableText(MR.strings.group_profile_is_stored_on_members_devices, TextAlign.Center) ReadableText(R.string.group_profile_is_stored_on_members_devices, TextAlign.Center)
Box( Box(
Modifier Modifier
.fillMaxWidth() .fillMaxWidth()
@@ -125,13 +124,13 @@ fun GroupProfileLayout(
} }
Row(Modifier.padding(bottom = DEFAULT_PADDING_HALF).fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { Row(Modifier.padding(bottom = DEFAULT_PADDING_HALF).fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
Text( Text(
stringResource(MR.strings.group_display_name_field), stringResource(R.string.group_display_name_field),
fontSize = 16.sp fontSize = 16.sp
) )
if (!isValidDisplayName(displayName.value)) { if (!isValidDisplayName(displayName.value)) {
Spacer(Modifier.size(DEFAULT_PADDING_HALF)) Spacer(Modifier.size(DEFAULT_PADDING_HALF))
Text( Text(
stringResource(MR.strings.no_spaces), stringResource(R.string.no_spaces),
fontSize = 16.sp, fontSize = 16.sp,
color = Color.Red color = Color.Red
) )
@@ -140,7 +139,7 @@ fun GroupProfileLayout(
ProfileNameField(displayName, "", ::isValidDisplayName, focusRequester) ProfileNameField(displayName, "", ::isValidDisplayName, focusRequester)
Spacer(Modifier.height(DEFAULT_PADDING)) Spacer(Modifier.height(DEFAULT_PADDING))
Text( Text(
stringResource(MR.strings.group_full_name_field), stringResource(R.string.group_full_name_field),
fontSize = 16.sp, fontSize = 16.sp,
modifier = Modifier.padding(bottom = DEFAULT_PADDING_HALF) modifier = Modifier.padding(bottom = DEFAULT_PADDING_HALF)
) )
@@ -149,7 +148,7 @@ fun GroupProfileLayout(
val enabled = !dataUnchanged && displayName.value.isNotEmpty() && isValidDisplayName(displayName.value) val enabled = !dataUnchanged && displayName.value.isNotEmpty() && isValidDisplayName(displayName.value)
if (enabled) { if (enabled) {
Text( Text(
stringResource(MR.strings.save_group_profile), stringResource(R.string.save_group_profile),
modifier = Modifier.clickable { modifier = Modifier.clickable {
saveProfile( saveProfile(
groupProfile.copy( groupProfile.copy(
@@ -163,7 +162,7 @@ fun GroupProfileLayout(
) )
} else { } else {
Text( Text(
stringResource(MR.strings.save_group_profile), stringResource(R.string.save_group_profile),
color = MaterialTheme.colors.secondary color = MaterialTheme.colors.secondary
) )
} }
@@ -183,9 +182,9 @@ fun GroupProfileLayout(
private fun showUnsavedChangesAlert(save: () -> Unit, revert: () -> Unit) { private fun showUnsavedChangesAlert(save: () -> Unit, revert: () -> Unit) {
AlertManager.shared.showAlertDialogStacked( AlertManager.shared.showAlertDialogStacked(
title = generalGetString(MR.strings.save_preferences_question), title = generalGetString(R.string.save_preferences_question),
confirmText = generalGetString(MR.strings.save_and_notify_group_members), confirmText = generalGetString(R.string.save_and_notify_group_members),
dismissText = generalGetString(MR.strings.exit_without_saving), dismissText = generalGetString(R.string.exit_without_saving),
onConfirm = save, onConfirm = save,
onDismiss = revert, onDismiss = revert,
) )
@@ -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,
)
}
@@ -6,14 +6,13 @@ 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 dev.icerock.moko.resources.compose.painterResource import androidx.compose.ui.res.painterResource
import dev.icerock.moko.resources.compose.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.* import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.* import chat.simplex.app.ui.theme.*
import chat.simplex.res.MR
@Composable @Composable
fun CICallItemView(cInfo: ChatInfo, cItem: ChatItem, status: CICallStatus, duration: Int, acceptCall: (Contact) -> Unit) { fun CICallItemView(cInfo: ChatInfo, cItem: ChatItem, status: CICallStatus, duration: Int, acceptCall: (Contact) -> Unit) {
@@ -22,20 +21,20 @@ 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(painterResource(MR.images.ic_settings_phone), stringResource(MR.strings.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(painterResource(MR.images.ic_call), stringResource(MR.strings.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(painterResource(MR.images.ic_call), stringResource(MR.strings.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(painterResource(MR.images.ic_call_end), stringResource(MR.strings.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(painterResource(MR.images.ic_phone_in_talk_filled), stringResource(MR.strings.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(painterResource(MR.images.ic_call_end), stringResource(MR.strings.icon_descr_call_ended), tint = MaterialTheme.colors.secondary, 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 = MaterialTheme.colors.secondary) Text(durationText(duration), color = MaterialTheme.colors.secondary)
} }
CICallStatus.Error -> {} CICallStatus.Error -> {}
@@ -53,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(MR.strings.answer_call), painterResource(MR.images.ic_ring_volume)) { acceptCall(cInfo.contact) } SimpleButton(stringResource(R.string.answer_call), painterResource(R.drawable.ic_ring_volume)) { acceptCall(cInfo.contact) }
} else { } else {
Icon(painterResource(MR.images.ic_ring_volume), stringResource(MR.strings.answer_call), tint = MaterialTheme.colors.secondary) 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 {
@@ -12,7 +12,6 @@ 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.views.helpers.generalGetString import chat.simplex.app.views.helpers.generalGetString
import chat.simplex.res.MR
@Composable @Composable
fun CIFeaturePreferenceView( fun CIFeaturePreferenceView(
@@ -31,7 +30,7 @@ fun CIFeaturePreferenceView(
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
val acceptTextId = if (setParam) MR.strings.accept_feature_set_1_day else MR.strings.accept_feature val acceptTextId = if (setParam) R.string.accept_feature_set_1_day else R.string.accept_feature
val param = if (setParam) 86400 else null val param = if (setParam) 86400 else null
val annotatedText = buildAnnotatedString { val annotatedText = buildAnnotatedString {
withStyle(chatEventStyle) { append(chatItem.content.text + " ") } withStyle(chatEventStyle) { append(chatItem.content.text + " ") }
@@ -14,8 +14,8 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.painter.Painter 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.platform.LocalDensity
import dev.icerock.moko.resources.compose.painterResource import androidx.compose.ui.res.painterResource
import dev.icerock.moko.resources.compose.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
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
@@ -23,7 +23,6 @@ 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.*
import chat.simplex.res.MR
import kotlinx.datetime.Clock import kotlinx.datetime.Clock
@Composable @Composable
@@ -33,7 +32,7 @@ fun CIFileView(
receiveFile: (Long) -> Unit receiveFile: (Long) -> Unit
) { ) {
val context = LocalContext.current val context = LocalContext.current
val saveFileLauncher = rememberSaveFileLauncher(ciFile = file) val saveFileLauncher = rememberSaveFileLauncher(cxt = context, ciFile = file)
@Composable @Composable
fun fileIcon( fun fileIcon(
@@ -44,15 +43,15 @@ fun CIFileView(
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
Icon( Icon(
painterResource(MR.images.ic_draft_filled), painterResource(R.drawable.ic_draft_filled),
stringResource(MR.strings.icon_descr_file), stringResource(R.string.icon_descr_file),
Modifier.fillMaxSize(), Modifier.fillMaxSize(),
tint = color tint = color
) )
if (innerIcon != null) { if (innerIcon != null) {
Icon( Icon(
innerIcon, innerIcon,
stringResource(MR.strings.icon_descr_file), stringResource(R.string.icon_descr_file),
Modifier Modifier
.size(32.dp) .size(32.dp)
.padding(top = 12.dp), .padding(top = 12.dp),
@@ -77,8 +76,8 @@ fun CIFileView(
receiveFile(file.fileId) receiveFile(file.fileId)
} else { } else {
AlertManager.shared.showAlertMsg( AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.large_file), generalGetString(R.string.large_file),
String.format(generalGetString(MR.strings.contact_sent_large_file), formatBytes(getMaxFileSize(file.fileProtocol))) String.format(generalGetString(R.string.contact_sent_large_file), formatBytes(getMaxFileSize(file.fileProtocol)))
) )
} }
} }
@@ -86,21 +85,21 @@ fun CIFileView(
when (file.fileProtocol) { when (file.fileProtocol) {
FileProtocol.XFTP -> FileProtocol.XFTP ->
AlertManager.shared.showAlertMsg( AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.waiting_for_file), generalGetString(R.string.waiting_for_file),
generalGetString(MR.strings.file_will_be_received_when_contact_completes_uploading) generalGetString(R.string.file_will_be_received_when_contact_completes_uploading)
) )
FileProtocol.SMP -> FileProtocol.SMP ->
AlertManager.shared.showAlertMsg( AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.waiting_for_file), generalGetString(R.string.waiting_for_file),
generalGetString(MR.strings.file_will_be_received_when_contact_is_online) generalGetString(R.string.file_will_be_received_when_contact_is_online)
) )
} }
is CIFileStatus.RcvComplete -> { is CIFileStatus.RcvComplete -> {
val filePath = getLoadedFilePath(file) val filePath = getLoadedFilePath(context, file)
if (filePath != null) { if (filePath != null) {
saveFileLauncher.launch(file.fileName) saveFileLauncher.launch(file.fileName)
} else { } else {
Toast.makeText(context, generalGetString(MR.strings.file_not_found), Toast.LENGTH_SHORT).show() Toast.makeText(context, generalGetString(R.string.file_not_found), Toast.LENGTH_SHORT).show()
} }
} }
else -> {} else -> {}
@@ -152,15 +151,15 @@ fun CIFileView(
FileProtocol.XFTP -> progressCircle(file.fileStatus.sndProgress, file.fileStatus.sndTotal) FileProtocol.XFTP -> progressCircle(file.fileStatus.sndProgress, file.fileStatus.sndTotal)
FileProtocol.SMP -> progressIndicator() FileProtocol.SMP -> progressIndicator()
} }
is CIFileStatus.SndComplete -> fileIcon(innerIcon = painterResource(MR.images.ic_check_filled)) is CIFileStatus.SndComplete -> fileIcon(innerIcon = painterResource(R.drawable.ic_check_filled))
is CIFileStatus.SndCancelled -> fileIcon(innerIcon = painterResource(MR.images.ic_close)) is CIFileStatus.SndCancelled -> fileIcon(innerIcon = painterResource(R.drawable.ic_close))
is CIFileStatus.SndError -> fileIcon(innerIcon = painterResource(MR.images.ic_close)) is CIFileStatus.SndError -> fileIcon(innerIcon = painterResource(R.drawable.ic_close))
is CIFileStatus.RcvInvitation -> is CIFileStatus.RcvInvitation ->
if (fileSizeValid()) if (fileSizeValid())
fileIcon(innerIcon = painterResource(MR.images.ic_arrow_downward), color = MaterialTheme.colors.primary) fileIcon(innerIcon = painterResource(R.drawable.ic_arrow_downward), color = MaterialTheme.colors.primary)
else else
fileIcon(innerIcon = painterResource(MR.images.ic_priority_high), color = WarningOrange) fileIcon(innerIcon = painterResource(R.drawable.ic_priority_high), color = WarningOrange)
is CIFileStatus.RcvAccepted -> fileIcon(innerIcon = painterResource(MR.images.ic_more_horiz)) is CIFileStatus.RcvAccepted -> fileIcon(innerIcon = painterResource(R.drawable.ic_more_horiz))
is CIFileStatus.RcvTransfer -> is CIFileStatus.RcvTransfer ->
if (file.fileProtocol == FileProtocol.XFTP && file.fileStatus.rcvProgress < file.fileStatus.rcvTotal) { if (file.fileProtocol == FileProtocol.XFTP && file.fileStatus.rcvProgress < file.fileStatus.rcvTotal) {
progressCircle(file.fileStatus.rcvProgress, file.fileStatus.rcvTotal) progressCircle(file.fileStatus.rcvProgress, file.fileStatus.rcvTotal)
@@ -168,9 +167,8 @@ fun CIFileView(
progressIndicator() progressIndicator()
} }
is CIFileStatus.RcvComplete -> fileIcon() is CIFileStatus.RcvComplete -> fileIcon()
is CIFileStatus.RcvCancelled -> fileIcon(innerIcon = painterResource(MR.images.ic_close)) is CIFileStatus.RcvCancelled -> fileIcon(innerIcon = painterResource(R.drawable.ic_close))
is CIFileStatus.RcvError -> fileIcon(innerIcon = painterResource(MR.images.ic_close)) is CIFileStatus.RcvError -> fileIcon(innerIcon = painterResource(R.drawable.ic_close))
is CIFileStatus.Invalid -> fileIcon(innerIcon = painterResource(MR.images.ic_question_mark))
} }
} else { } else {
fileIcon() fileIcon()
@@ -213,7 +211,6 @@ class ChatItemProvider: PreviewParameterProvider<ChatItem> {
meta = CIMeta.getSample(1, Clock.System.now(), "", CIStatus.SndSent(), itemEdited = true), 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,
reactions = listOf(),
file = CIFile.getSample(fileStatus = CIFileStatus.SndComplete) file = CIFile.getSample(fileStatus = CIFileStatus.SndComplete)
) )
private val fileChatItemWtFile = ChatItem( private val fileChatItemWtFile = ChatItem(
@@ -221,7 +218,6 @@ class ChatItemProvider: PreviewParameterProvider<ChatItem> {
meta = CIMeta.getSample(1, Clock.System.now(), "", CIStatus.RcvRead(), ), 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,
reactions = listOf(),
file = null file = null
) )
override val values = listOf( override val values = listOf(
@@ -8,7 +8,7 @@ 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 dev.icerock.moko.resources.compose.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.* import androidx.compose.ui.tooling.preview.*
@@ -18,7 +18,6 @@ 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.*
import chat.simplex.res.MR
@Composable @Composable
fun CIGroupInvitationView( fun CIGroupInvitationView(
@@ -44,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 = MR.images.ic_supervised_user_circle_filled, 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),
@@ -61,11 +60,11 @@ fun CIGroupInvitationView(
@Composable @Composable
fun groupInvitationText() { fun groupInvitationText() {
when { when {
sent -> Text(stringResource(MR.strings.you_sent_group_invitation)) sent -> Text(stringResource(R.string.you_sent_group_invitation))
!sent && groupInvitation.status == CIGroupInvitationStatus.Pending -> Text(stringResource(MR.strings.you_are_invited_to_group)) !sent && groupInvitation.status == CIGroupInvitationStatus.Pending -> Text(stringResource(R.string.you_are_invited_to_group))
!sent && groupInvitation.status == CIGroupInvitationStatus.Accepted -> Text(stringResource(MR.strings.you_joined_this_group)) !sent && groupInvitation.status == CIGroupInvitationStatus.Accepted -> Text(stringResource(R.string.you_joined_this_group))
!sent && groupInvitation.status == CIGroupInvitationStatus.Rejected -> Text(stringResource(MR.strings.you_rejected_group_invitation)) !sent && groupInvitation.status == CIGroupInvitationStatus.Rejected -> Text(stringResource(R.string.you_rejected_group_invitation))
!sent && groupInvitation.status == CIGroupInvitationStatus.Expired -> Text(stringResource(MR.strings.group_invitation_expired)) !sent && groupInvitation.status == CIGroupInvitationStatus.Expired -> Text(stringResource(R.string.group_invitation_expired))
} }
} }
@@ -96,7 +95,7 @@ fun CIGroupInvitationView(
if (action) { if (action) {
groupInvitationText() groupInvitationText()
Text(stringResource( Text(stringResource(
if (chatIncognito) MR.strings.group_invitation_tap_to_join_incognito else MR.strings.group_invitation_tap_to_join), if (chatIncognito) R.string.group_invitation_tap_to_join_incognito else R.string.group_invitation_tap_to_join),
color = if (chatIncognito) Indigo else MaterialTheme.colors.primary) color = if (chatIncognito) Indigo else MaterialTheme.colors.primary)
} else { } else {
Box(Modifier.padding(end = 48.dp)) { Box(Modifier.padding(end = 48.dp)) {
@@ -2,6 +2,7 @@ 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.*
@@ -17,8 +18,8 @@ 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 dev.icerock.moko.resources.compose.painterResource import androidx.compose.ui.res.painterResource
import dev.icerock.moko.resources.compose.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
@@ -31,8 +32,6 @@ import coil.compose.rememberAsyncImagePainter
import coil.decode.GifDecoder import coil.decode.GifDecoder
import coil.decode.ImageDecoderDecoder import coil.decode.ImageDecoderDecoder
import coil.request.ImageRequest import coil.request.ImageRequest
import chat.simplex.res.MR
import dev.icerock.moko.resources.StringResource
import java.io.File import java.io.File
@Composable @Composable
@@ -53,7 +52,7 @@ fun CIImageView(
} }
@Composable @Composable
fun fileIcon(icon: Painter, stringId: StringResource) { fun fileIcon(icon: Painter, @StringRes stringId: Int) {
Icon( Icon(
icon, icon,
stringResource(stringId), stringResource(stringId),
@@ -78,15 +77,14 @@ fun CIImageView(
FileProtocol.SMP -> {} FileProtocol.SMP -> {}
} }
is CIFileStatus.SndTransfer -> progressIndicator() is CIFileStatus.SndTransfer -> progressIndicator()
is CIFileStatus.SndComplete -> fileIcon(painterResource(MR.images.ic_check_filled), MR.strings.icon_descr_image_snd_complete) is CIFileStatus.SndComplete -> fileIcon(painterResource(R.drawable.ic_check_filled), R.string.icon_descr_image_snd_complete)
is CIFileStatus.SndCancelled -> fileIcon(painterResource(MR.images.ic_close), MR.strings.icon_descr_file) is CIFileStatus.SndCancelled -> fileIcon(painterResource(R.drawable.ic_close), R.string.icon_descr_file)
is CIFileStatus.SndError -> fileIcon(painterResource(MR.images.ic_close), MR.strings.icon_descr_file) is CIFileStatus.SndError -> fileIcon(painterResource(R.drawable.ic_close), R.string.icon_descr_file)
is CIFileStatus.RcvInvitation -> fileIcon(painterResource(MR.images.ic_arrow_downward), MR.strings.icon_descr_asked_to_receive) is CIFileStatus.RcvInvitation -> fileIcon(painterResource(R.drawable.ic_arrow_downward), R.string.icon_descr_asked_to_receive)
is CIFileStatus.RcvAccepted -> fileIcon(painterResource(MR.images.ic_more_horiz), MR.strings.icon_descr_waiting_for_image) is CIFileStatus.RcvAccepted -> fileIcon(painterResource(R.drawable.ic_more_horiz), R.string.icon_descr_waiting_for_image)
is CIFileStatus.RcvTransfer -> progressIndicator() is CIFileStatus.RcvTransfer -> progressIndicator()
is CIFileStatus.RcvCancelled -> fileIcon(painterResource(MR.images.ic_close), MR.strings.icon_descr_file) is CIFileStatus.RcvCancelled -> fileIcon(painterResource(R.drawable.ic_close), R.string.icon_descr_file)
is CIFileStatus.RcvError -> fileIcon(painterResource(MR.images.ic_close), MR.strings.icon_descr_file) is CIFileStatus.RcvError -> fileIcon(painterResource(R.drawable.ic_close), R.string.icon_descr_file)
is CIFileStatus.Invalid -> fileIcon(painterResource(MR.images.ic_question_mark), MR.strings.icon_descr_file)
else -> {} else -> {}
} }
} }
@@ -103,7 +101,7 @@ fun CIImageView(
fun imageView(imageBitmap: Bitmap, onClick: () -> Unit) { fun imageView(imageBitmap: Bitmap, onClick: () -> Unit) {
Image( Image(
imageBitmap.asImageBitmap(), imageBitmap.asImageBitmap(),
contentDescription = stringResource(MR.strings.image_descr), contentDescription = stringResource(R.string.image_descr),
// .width(1000.dp) is a hack for image to increase IntrinsicSize of FramedItemView // .width(1000.dp) is a hack for image to increase IntrinsicSize of FramedItemView
// if text is short and take all available width if text is long // if text is short and take all available width if text is long
modifier = Modifier modifier = Modifier
@@ -120,7 +118,7 @@ fun CIImageView(
fun imageView(painter: Painter, onClick: () -> Unit) { fun imageView(painter: Painter, onClick: () -> Unit) {
Image( Image(
painter, painter,
contentDescription = stringResource(MR.strings.image_descr), contentDescription = stringResource(R.string.image_descr),
// .width(1000.dp) is a hack for image to increase IntrinsicSize of FramedItemView // .width(1000.dp) is a hack for image to increase IntrinsicSize of FramedItemView
// if text is short and take all available width if text is long // if text is short and take all available width if text is long
modifier = Modifier modifier = Modifier
@@ -141,8 +139,8 @@ fun CIImageView(
} }
fun imageAndFilePath(file: CIFile?): Pair<Bitmap?, String?> { fun imageAndFilePath(file: CIFile?): Pair<Bitmap?, String?> {
val imageBitmap: Bitmap? = getLoadedImage(file) val imageBitmap: Bitmap? = getLoadedImage(SimplexApp.context, file)
val filePath = getLoadedFilePath(file) val filePath = getLoadedFilePath(SimplexApp.context, file)
return imageBitmap to filePath return imageBitmap to filePath
} }
@@ -162,7 +160,7 @@ fun CIImageView(
val view = LocalView.current val view = LocalView.current
imageView(imagePainter, onClick = { imageView(imagePainter, onClick = {
hideKeyboard(view) hideKeyboard(view)
if (getLoadedFilePath(file) != null) { if (getLoadedFilePath(context, file) != null) {
ModalManager.shared.showCustomModal(animated = false) { close -> ModalManager.shared.showCustomModal(animated = false) { close ->
ImageFullScreenView(imageProvider, close) ImageFullScreenView(imageProvider, close)
} }
@@ -177,21 +175,21 @@ fun CIImageView(
receiveFile(file.fileId) receiveFile(file.fileId)
} else { } else {
AlertManager.shared.showAlertMsg( AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.large_file), generalGetString(R.string.large_file),
String.format(generalGetString(MR.strings.contact_sent_large_file), formatBytes(getMaxFileSize(file.fileProtocol))) String.format(generalGetString(R.string.contact_sent_large_file), formatBytes(getMaxFileSize(file.fileProtocol)))
) )
} }
CIFileStatus.RcvAccepted -> CIFileStatus.RcvAccepted ->
when (file.fileProtocol) { when (file.fileProtocol) {
FileProtocol.XFTP -> FileProtocol.XFTP ->
AlertManager.shared.showAlertMsg( AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.waiting_for_image), generalGetString(R.string.waiting_for_image),
generalGetString(MR.strings.image_will_be_received_when_contact_completes_uploading) generalGetString(R.string.image_will_be_received_when_contact_completes_uploading)
) )
FileProtocol.SMP -> FileProtocol.SMP ->
AlertManager.shared.showAlertMsg( AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.waiting_for_image), generalGetString(R.string.waiting_for_image),
generalGetString(MR.strings.image_will_be_received_when_contact_is_online) generalGetString(R.string.image_will_be_received_when_contact_is_online)
) )
} }
CIFileStatus.RcvTransfer(rcvProgress = 7, rcvTotal = 10) -> {} // ? CIFileStatus.RcvTransfer(rcvProgress = 7, rcvTotal = 10) -> {} // ?
@@ -1,5 +1,6 @@
package chat.simplex.app.views.chat.item package chat.simplex.app.views.chat.item
import SectionSpacer
import SectionView import SectionView
import androidx.compose.foundation.* import androidx.compose.foundation.*
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
@@ -7,15 +8,15 @@ import androidx.compose.material.*
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 dev.icerock.moko.resources.compose.painterResource import androidx.compose.ui.platform.LocalContext
import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.res.painterResource
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
import chat.simplex.app.R import chat.simplex.app.R
import chat.simplex.app.ui.theme.DEFAULT_PADDING import chat.simplex.app.ui.theme.DEFAULT_PADDING
import chat.simplex.app.views.helpers.* import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.usersettings.SettingsActionItem import chat.simplex.app.views.usersettings.SettingsActionItem
import chat.simplex.res.MR
@Composable @Composable
fun CIInvalidJSONView(json: String) { fun CIInvalidJSONView(json: String) {
@@ -23,7 +24,7 @@ fun CIInvalidJSONView(json: String) {
.clickable { ModalManager.shared.showModal(true) { InvalidJSONView(json) } } .clickable { ModalManager.shared.showModal(true) { InvalidJSONView(json) } }
.padding(horizontal = 10.dp, vertical = 6.dp) .padding(horizontal = 10.dp, vertical = 6.dp)
) { ) {
Text(stringResource(MR.strings.invalid_data), color = Color.Red, fontStyle = FontStyle.Italic) Text(stringResource(R.string.invalid_data), color = Color.Red, fontStyle = FontStyle.Italic)
} }
} }
@@ -32,8 +33,9 @@ fun InvalidJSONView(json: String) {
Column { Column {
Spacer(Modifier.height(DEFAULT_PADDING)) Spacer(Modifier.height(DEFAULT_PADDING))
SectionView { SectionView {
SettingsActionItem(painterResource(MR.images.ic_share), generalGetString(MR.strings.share_verb), click = { val context = LocalContext.current
shareText(json) SettingsActionItem(painterResource(R.drawable.ic_share), generalGetString(R.string.share_verb), click = {
shareText(context, json)
}) })
} }
Column(Modifier.padding(DEFAULT_PADDING).fillMaxWidth().verticalScroll(rememberScrollState())) { Column(Modifier.padding(DEFAULT_PADDING).fillMaxWidth().verticalScroll(rememberScrollState())) {
@@ -3,17 +3,18 @@ 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.ui.graphics.painter.Painter import androidx.compose.ui.graphics.painter.Painter
import dev.icerock.moko.resources.compose.painterResource import androidx.compose.ui.res.painterResource
import chat.simplex.app.R
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.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.* import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import chat.simplex.app.model.* import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.CurrentColors import chat.simplex.app.ui.theme.CurrentColors
import chat.simplex.res.MR
import kotlinx.datetime.Clock import kotlinx.datetime.Clock
@Composable @Composable
@@ -23,7 +24,7 @@ fun CIMetaView(chatItem: ChatItem, timedMessagesTTL: Int?, metaColor: Color = Ma
Text( Text(
chatItem.timestampText, chatItem.timestampText,
color = metaColor, color = metaColor,
fontSize = 12.sp, fontSize = 14.sp,
modifier = Modifier.padding(start = 3.dp) modifier = Modifier.padding(start = 3.dp)
) )
} else { } else {
@@ -36,31 +37,27 @@ fun CIMetaView(chatItem: ChatItem, timedMessagesTTL: Int?, metaColor: Color = Ma
// 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(painterResource(MR.images.ic_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(painterResource(MR.images.ic_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(shortTimeText(ttl), color = color, fontSize = 12.sp) Text(TimedMessagesPreference.shortTtlText(ttl), color = color, fontSize = 13.sp)
} }
Spacer(Modifier.width(4.dp)) Spacer(Modifier.width(4.dp))
} }
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
if (meta.itemStatus is CIStatus.SndSent || meta.itemStatus is CIStatus.SndRcvd) { StatusIconText(painterResource(icon), statusColor)
Icon(painterResource(icon), null, Modifier.height(17.dp), tint = statusColor)
} else {
StatusIconText(painterResource(icon), statusColor)
}
Spacer(Modifier.width(4.dp)) Spacer(Modifier.width(4.dp))
} else if (!meta.disappearing) { } else if (!meta.disappearing) {
StatusIconText(painterResource(MR.images.ic_circle_filled), 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 = 12.sp, maxLines = 1, overflow = TextOverflow.Ellipsis) 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
@@ -72,7 +69,7 @@ fun reserveSpaceForMeta(meta: CIMeta, chatTTL: Int?): String {
res += iconSpace res += iconSpace
val ttl = meta.itemTimed.ttl val ttl = meta.itemTimed.ttl
if (ttl != chatTTL) { if (ttl != chatTTL) {
res += shortTimeText(ttl) res += TimedMessagesPreference.shortTtlText(ttl)
} }
} }
if (meta.statusIcon(CurrentColors.value.colors.secondary) != null || !meta.disappearing) { if (meta.statusIcon(CurrentColors.value.colors.secondary) != null || !meta.disappearing) {
@@ -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)
}
)
}
}
@@ -3,6 +3,7 @@ package chat.simplex.app.views.chat.item
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.Rect import android.graphics.Rect
import android.net.Uri import android.net.Uri
import androidx.annotation.StringRes
import androidx.compose.foundation.* import androidx.compose.foundation.*
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CornerSize import androidx.compose.foundation.shape.CornerSize
@@ -16,8 +17,8 @@ import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.layout.* import androidx.compose.ui.layout.*
import androidx.compose.ui.platform.* import androidx.compose.ui.platform.*
import dev.icerock.moko.resources.compose.painterResource import androidx.compose.ui.res.painterResource
import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.* import androidx.compose.ui.unit.*
import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
@@ -29,8 +30,6 @@ import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.helpers.* import chat.simplex.app.views.helpers.*
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout.RESIZE_MODE_FIXED_WIDTH import com.google.android.exoplayer2.ui.AspectRatioFrameLayout.RESIZE_MODE_FIXED_WIDTH
import com.google.android.exoplayer2.ui.StyledPlayerView import com.google.android.exoplayer2.ui.StyledPlayerView
import chat.simplex.res.MR
import dev.icerock.moko.resources.StringResource
import java.io.File import java.io.File
@Composable @Composable
@@ -47,7 +46,7 @@ fun CIVideoView(
contentAlignment = Alignment.TopEnd contentAlignment = Alignment.TopEnd
) { ) {
val context = LocalContext.current val context = LocalContext.current
val filePath = remember(file) { getLoadedFilePath(file) } val filePath = remember(file) { getLoadedFilePath(SimplexApp.context, file) }
val preview = remember(image) { base64ToBitmap(image) } val preview = remember(image) { base64ToBitmap(image) }
if (file != null && filePath != null) { if (file != null && filePath != null) {
val uri = remember(filePath) { FileProvider.getUriForFile(context, "${BuildConfig.APPLICATION_ID}.provider", File(filePath)) } val uri = remember(filePath) { FileProvider.getUriForFile(context, "${BuildConfig.APPLICATION_ID}.provider", File(filePath)) }
@@ -69,14 +68,14 @@ fun CIVideoView(
when (file.fileProtocol) { when (file.fileProtocol) {
FileProtocol.XFTP -> FileProtocol.XFTP ->
AlertManager.shared.showAlertMsg( AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.waiting_for_video), generalGetString(R.string.waiting_for_video),
generalGetString(MR.strings.video_will_be_received_when_contact_completes_uploading) generalGetString(R.string.video_will_be_received_when_contact_completes_uploading)
) )
FileProtocol.SMP -> FileProtocol.SMP ->
AlertManager.shared.showAlertMsg( AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.waiting_for_video), generalGetString(R.string.waiting_for_video),
generalGetString(MR.strings.video_will_be_received_when_contact_is_online) generalGetString(R.string.video_will_be_received_when_contact_is_online)
) )
} }
CIFileStatus.RcvTransfer(rcvProgress = 7, rcvTotal = 10) -> {} // ? CIFileStatus.RcvTransfer(rcvProgress = 7, rcvTotal = 10) -> {} // ?
@@ -101,7 +100,7 @@ fun CIVideoView(
@Composable @Composable
private fun VideoView(uri: Uri, file: CIFile, defaultPreview: Bitmap, defaultDuration: Long, showMenu: MutableState<Boolean>, onClick: () -> Unit) { private fun VideoView(uri: Uri, file: CIFile, defaultPreview: Bitmap, defaultDuration: Long, showMenu: MutableState<Boolean>, onClick: () -> Unit) {
val context = LocalContext.current val context = LocalContext.current
val player = remember(uri) { VideoPlayer.getOrCreate(uri, false, defaultPreview, defaultDuration, true) } val player = remember(uri) { VideoPlayer.getOrCreate(uri, false, defaultPreview, defaultDuration, true, context) }
val videoPlaying = remember(uri.path) { player.videoPlaying } val videoPlaying = remember(uri.path) { player.videoPlaying }
val progress = remember(uri.path) { player.progress } val progress = remember(uri.path) { player.progress }
val duration = remember(uri.path) { player.duration } val duration = remember(uri.path) { player.duration }
@@ -162,7 +161,7 @@ private fun BoxScope.PlayButton(error: Boolean = false, onLongClick: () -> Unit,
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
Icon( Icon(
painterResource(MR.images.ic_play_arrow_filled), painterResource(R.drawable.ic_play_arrow_filled),
contentDescription = null, contentDescription = null,
tint = if (error) WarningOrange else Color.White tint = if (error) WarningOrange else Color.White
) )
@@ -190,7 +189,7 @@ private fun DurationProgress(file: CIFile, playing: MutableState<Boolean>, durat
color = Color.White color = Color.White
) )
/*if (!soundEnabled.value) { /*if (!soundEnabled.value) {
Icon(painterResource(MR.images.ic_volume_off_filled), null, Icon(painterResource(R.drawable.ic_volume_off_filled), null,
Modifier.padding(start = 5.dp).size(10.dp), Modifier.padding(start = 5.dp).size(10.dp),
tint = Color.White tint = Color.White
) )
@@ -221,7 +220,7 @@ private fun ImageView(preview: Bitmap, showMenu: MutableState<Boolean>, onClick:
val width = remember(preview) { if (preview.width * 0.97 <= preview.height) videoViewFullWidth(windowWidth) * 0.75f else 1000.dp } val width = remember(preview) { if (preview.width * 0.97 <= preview.height) videoViewFullWidth(windowWidth) * 0.75f else 1000.dp }
Image( Image(
preview.asImageBitmap(), preview.asImageBitmap(),
contentDescription = stringResource(MR.strings.video_descr), contentDescription = stringResource(R.string.video_descr),
modifier = Modifier modifier = Modifier
.width(width) .width(width)
.combinedClickable( .combinedClickable(
@@ -253,7 +252,7 @@ private fun progressIndicator() {
} }
@Composable @Composable
private fun fileIcon(icon: Painter, stringId: StringResource) { private fun fileIcon(icon: Painter, @StringRes stringId: Int) {
Icon( Icon(
icon, icon,
stringResource(stringId), stringResource(stringId),
@@ -296,20 +295,19 @@ private fun loadingIndicator(file: CIFile?) {
FileProtocol.XFTP -> progressCircle(file.fileStatus.sndProgress, file.fileStatus.sndTotal) FileProtocol.XFTP -> progressCircle(file.fileStatus.sndProgress, file.fileStatus.sndTotal)
FileProtocol.SMP -> progressIndicator() FileProtocol.SMP -> progressIndicator()
} }
is CIFileStatus.SndComplete -> fileIcon(painterResource(MR.images.ic_check_filled), MR.strings.icon_descr_video_snd_complete) is CIFileStatus.SndComplete -> fileIcon(painterResource(R.drawable.ic_check_filled), R.string.icon_descr_video_snd_complete)
is CIFileStatus.SndCancelled -> fileIcon(painterResource(MR.images.ic_close), MR.strings.icon_descr_file) is CIFileStatus.SndCancelled -> fileIcon(painterResource(R.drawable.ic_close), R.string.icon_descr_file)
is CIFileStatus.SndError -> fileIcon(painterResource(MR.images.ic_close), MR.strings.icon_descr_file) is CIFileStatus.SndError -> fileIcon(painterResource(R.drawable.ic_close), R.string.icon_descr_file)
is CIFileStatus.RcvInvitation -> fileIcon(painterResource(MR.images.ic_arrow_downward), MR.strings.icon_descr_video_asked_to_receive) is CIFileStatus.RcvInvitation -> fileIcon(painterResource(R.drawable.ic_arrow_downward), R.string.icon_descr_video_asked_to_receive)
is CIFileStatus.RcvAccepted -> fileIcon(painterResource(MR.images.ic_more_horiz), MR.strings.icon_descr_waiting_for_video) is CIFileStatus.RcvAccepted -> fileIcon(painterResource(R.drawable.ic_more_horiz), R.string.icon_descr_waiting_for_video)
is CIFileStatus.RcvTransfer -> is CIFileStatus.RcvTransfer ->
if (file.fileProtocol == FileProtocol.XFTP && file.fileStatus.rcvProgress < file.fileStatus.rcvTotal) { if (file.fileProtocol == FileProtocol.XFTP && file.fileStatus.rcvProgress < file.fileStatus.rcvTotal) {
progressCircle(file.fileStatus.rcvProgress, file.fileStatus.rcvTotal) progressCircle(file.fileStatus.rcvProgress, file.fileStatus.rcvTotal)
} else { } else {
progressIndicator() progressIndicator()
} }
is CIFileStatus.RcvCancelled -> fileIcon(painterResource(MR.images.ic_close), MR.strings.icon_descr_file) is CIFileStatus.RcvCancelled -> fileIcon(painterResource(R.drawable.ic_close), R.string.icon_descr_file)
is CIFileStatus.RcvError -> fileIcon(painterResource(MR.images.ic_close), MR.strings.icon_descr_file) is CIFileStatus.RcvError -> fileIcon(painterResource(R.drawable.ic_close), R.string.icon_descr_file)
is CIFileStatus.Invalid -> fileIcon(painterResource(MR.images.ic_question_mark), MR.strings.icon_descr_file)
else -> {} else -> {}
} }
} }
@@ -328,8 +326,8 @@ private fun receiveFileIfValidSize(file: CIFile, receiveFile: (Long) -> Unit) {
receiveFile(file.fileId) receiveFile(file.fileId)
} else { } else {
AlertManager.shared.showAlertMsg( AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.large_file), generalGetString(R.string.large_file),
String.format(generalGetString(MR.strings.contact_sent_large_file), formatBytes(getMaxFileSize(file.fileProtocol))) String.format(generalGetString(R.string.contact_sent_large_file), formatBytes(getMaxFileSize(file.fileProtocol)))
) )
} }
} }
@@ -9,19 +9,20 @@ 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.draw.* import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.drawWithCache
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.* import androidx.compose.ui.graphics.*
import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.platform.* import androidx.compose.ui.platform.LocalContext
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.*
import chat.simplex.res.MR
import dev.icerock.moko.resources.compose.painterResource
import kotlinx.coroutines.flow.distinctUntilChanged
// TODO refactor https://github.com/simplex-chat/simplex-chat/pull/1451#discussion_r1033429901 // TODO refactor https://github.com/simplex-chat/simplex-chat/pull/1451#discussion_r1033429901
@@ -35,15 +36,14 @@ fun CIVoiceView(
ci: ChatItem, ci: ChatItem,
timedMessagesTTL: Int?, timedMessagesTTL: Int?,
longClick: () -> Unit, longClick: () -> Unit,
receiveFile: (Long) -> Unit,
) { ) {
Row( Row(
Modifier.padding(top = if (hasText) 14.dp else 4.dp, bottom = if (hasText) 14.dp else 6.dp, start = if (hasText) 6.dp else 0.dp, end = if (hasText) 6.dp else 0.dp), Modifier.padding(top = if (hasText) 14.dp else 4.dp, bottom = if (hasText) 14.dp else 6.dp, start = 6.dp, end = 6.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
if (file != null) { if (file != null) {
val context = LocalContext.current val context = LocalContext.current
val filePath = remember(file.filePath, file.fileStatus) { getLoadedFilePath(file) } val filePath = remember(file.filePath, file.fileStatus) { getLoadedFilePath(context, file) }
var brokenAudio by rememberSaveable(file.filePath) { mutableStateOf(false) } var brokenAudio by rememberSaveable(file.filePath) { mutableStateOf(false) }
val audioPlaying = rememberSaveable(file.filePath) { mutableStateOf(false) } val audioPlaying = rememberSaveable(file.filePath) { mutableStateOf(false) }
val progress = rememberSaveable(file.filePath) { mutableStateOf(0) } val progress = rememberSaveable(file.filePath) { mutableStateOf(0) }
@@ -64,11 +64,9 @@ fun CIVoiceView(
durationText(time / 1000) durationText(time / 1000)
} }
} }
VoiceLayout(file, ci, text, audioPlaying, progress, duration, brokenAudio, sent, hasText, timedMessagesTTL, play, pause, longClick, receiveFile) { VoiceLayout(file, ci, text, audioPlaying, progress, duration, brokenAudio, sent, hasText, timedMessagesTTL, play, pause, longClick)
AudioPlayer.seekTo(it, progress, filePath)
}
} else { } else {
VoiceMsgIndicator(null, false, sent, hasText, null, null, false, {}, {}, longClick, receiveFile) VoiceMsgIndicator(null, false, sent, hasText, null, null, false, {}, {}, longClick)
val metaReserve = if (edited) val metaReserve = if (edited)
" " " "
else else
@@ -92,89 +90,39 @@ private fun VoiceLayout(
timedMessagesTTL: Int?, timedMessagesTTL: Int?,
play: () -> Unit, play: () -> Unit,
pause: () -> Unit, pause: () -> Unit,
longClick: () -> Unit, longClick: () -> Unit
receiveFile: (Long) -> Unit,
onProgressChanged: (Int) -> Unit,
) { ) {
@Composable
fun RowScope.Slider(backgroundColor: Color, padding: PaddingValues = PaddingValues(horizontal = DEFAULT_PADDING_HALF)) {
var movedManuallyTo by rememberSaveable(file.fileId) { mutableStateOf(-1) }
if (audioPlaying.value || progress.value > 0 || movedManuallyTo == progress.value) {
val dp4 = with(LocalDensity.current) { 4.dp.toPx() }
val dp10 = with(LocalDensity.current) { 10.dp.toPx() }
val primary = MaterialTheme.colors.primary
val inactiveTrackColor =
MaterialTheme.colors.primary.mixWith(
backgroundColor.copy(1f).mixWith(MaterialTheme.colors.background, backgroundColor.alpha),
0.24f)
val width = with(LocalDensity.current) { LocalView.current.width.toDp() }
val colors = SliderDefaults.colors(
inactiveTrackColor = inactiveTrackColor
)
Slider(
progress.value.toFloat(),
onValueChange = {
onProgressChanged(it.toInt())
movedManuallyTo = it.toInt()
},
Modifier
.size(width, 48.dp)
.weight(1f)
.padding(padding)
.drawBehind {
drawRect(primary, Offset(0f, (size.height - dp4) / 2), size = androidx.compose.ui.geometry.Size(dp10, dp4))
drawRect(inactiveTrackColor, Offset(size.width - dp10, (size.height - dp4) / 2), size = androidx.compose.ui.geometry.Size(dp10, dp4))
},
valueRange = 0f..duration.value.toFloat(),
colors = colors
)
LaunchedEffect(Unit) {
snapshotFlow { audioPlaying.value }
.distinctUntilChanged()
.collect {
movedManuallyTo = -1
}
}
}
}
when { when {
hasText -> { hasText -> {
val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage
val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage
Spacer(Modifier.width(6.dp)) Spacer(Modifier.width(6.dp))
VoiceMsgIndicator(file, audioPlaying.value, sent, hasText, progress, duration, brokenAudio, play, pause, longClick, receiveFile) VoiceMsgIndicator(file, audioPlaying.value, sent, hasText, progress, duration, brokenAudio, play, pause, longClick)
Row(verticalAlignment = Alignment.CenterVertically) { DurationText(text, PaddingValues(start = 12.dp))
DurationText(text, PaddingValues(start = 12.dp))
Slider(if (ci.chatDir.sent) sentColor else receivedColor)
}
} }
sent -> { sent -> {
Column(horizontalAlignment = Alignment.End) { Row {
Row { Row(verticalAlignment = Alignment.CenterVertically) {
Row(Modifier.weight(1f, false), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.End) { Spacer(Modifier.height(56.dp))
Spacer(Modifier.height(56.dp)) DurationText(text, PaddingValues(end = 12.dp))
Slider(MaterialTheme.colors.background, PaddingValues(end = DEFAULT_PADDING_HALF + 3.dp))
DurationText(text, PaddingValues(end = 12.dp))
}
VoiceMsgIndicator(file, audioPlaying.value, sent, hasText, progress, duration, brokenAudio, play, pause, longClick, receiveFile)
} }
Box(Modifier.padding(top = 6.dp, end = 6.dp)) { Column {
CIMetaView(ci, timedMessagesTTL) VoiceMsgIndicator(file, audioPlaying.value, sent, hasText, progress, duration, brokenAudio, play, pause, longClick)
Box(Modifier.align(Alignment.CenterHorizontally).padding(top = 6.dp)) {
CIMetaView(ci, timedMessagesTTL)
}
} }
} }
} }
else -> { else -> {
Column(horizontalAlignment = Alignment.Start) { Row {
Row { Column {
VoiceMsgIndicator(file, audioPlaying.value, sent, hasText, progress, duration, brokenAudio, play, pause, longClick, receiveFile) VoiceMsgIndicator(file, audioPlaying.value, sent, hasText, progress, duration, brokenAudio, play, pause, longClick)
Row(Modifier.weight(1f, false), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Start) { Box(Modifier.align(Alignment.CenterHorizontally).padding(top = 6.dp)) {
DurationText(text, PaddingValues(start = 12.dp)) CIMetaView(ci, timedMessagesTTL)
Slider(MaterialTheme.colors.background, PaddingValues(start = DEFAULT_PADDING_HALF + 3.dp))
Spacer(Modifier.height(56.dp))
} }
} }
Box(Modifier.padding(top = 6.dp)) { Row(verticalAlignment = Alignment.CenterVertically) {
CIMetaView(ci, timedMessagesTTL) DurationText(text, PaddingValues(start = 12.dp))
Spacer(Modifier.height(56.dp))
} }
} }
} }
@@ -225,7 +173,7 @@ private fun PlayPauseButton(
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
Icon( Icon(
if (audioPlaying) painterResource(MR.images.ic_pause_filled) else painterResource(MR.images.ic_play_arrow_filled), 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) MaterialTheme.colors.secondary else MaterialTheme.colors.primary tint = if (error) WarningOrange else if (!enabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary
@@ -245,8 +193,7 @@ private fun VoiceMsgIndicator(
error: Boolean, error: Boolean,
play: () -> Unit, play: () -> Unit,
pause: () -> Unit, pause: () -> Unit,
longClick: () -> Unit, longClick: () -> Unit
receiveFile: (Long) -> Unit,
) { ) {
val strokeWidth = with(LocalDensity.current) { 3.dp.toPx() } val strokeWidth = with(LocalDensity.current) { 3.dp.toPx() }
val strokeColor = MaterialTheme.colors.primary val strokeColor = MaterialTheme.colors.primary
@@ -255,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(
if (audioPlaying) painterResource(MR.images.ic_pause_filled) else painterResource(MR.images.ic_play_arrow_filled), 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
@@ -265,9 +212,8 @@ 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 is CIFileStatus.RcvInvitation) { if (file?.fileStatus is CIFileStatus.RcvInvitation
PlayPauseButton(audioPlaying, sent, 0f, strokeWidth, strokeColor, true, error, { receiveFile(file.fileId) }, {}, longClick = longClick) || file?.fileStatus is CIFileStatus.RcvTransfer
} else if (file?.fileStatus is CIFileStatus.RcvTransfer
|| file?.fileStatus is CIFileStatus.RcvAccepted || file?.fileStatus is CIFileStatus.RcvAccepted
) { ) {
Box( Box(
@@ -0,0 +1,457 @@
package chat.simplex.app.views.chat.item
import android.Manifest
import android.os.Build
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.*
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
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.chat.ComposeContextItem
import chat.simplex.app.views.chat.ComposeState
import chat.simplex.app.views.helpers.*
import com.google.accompanist.permissions.rememberPermissionState
import kotlinx.datetime.Clock
// TODO refactor so that FramedItemView can show all CIContent items if they're deleted (see Swift code)
@Composable
fun ChatItemView(
cInfo: ChatInfo,
cItem: ChatItem,
composeState: MutableState<ComposeState>,
imageProvider: (() -> ImageGalleryProvider)? = null,
showMember: Boolean = false,
useLinkPreviews: Boolean,
linkMode: SimplexLinkMode,
deleteMessage: (Long, CIDeleteMode) -> Unit,
receiveFile: (Long) -> Unit,
cancelFile: (Long) -> Unit,
joinGroup: (Long) -> Unit,
acceptCall: (Contact) -> Unit,
scrollToItem: (Long) -> Unit,
acceptFeature: (Contact, ChatFeature, Int?) -> Unit
) {
val context = LocalContext.current
val uriHandler = LocalUriHandler.current
val sent = cItem.chatDir.sent
val alignment = if (sent) Alignment.CenterEnd else Alignment.CenterStart
val showMenu = remember { mutableStateOf(false) }
val revealed = remember { mutableStateOf(false) }
val fullDeleteAllowed = remember(cInfo) { cInfo.featureEnabled(ChatFeature.FullDelete) }
val saveFileLauncher = rememberSaveFileLauncher(cxt = context, ciFile = cItem.file)
val onLinkLongClick = { _: String -> showMenu.value = true }
val live = composeState.value.liveMessage != null
Box(
modifier = Modifier
.padding(bottom = 4.dp)
.fillMaxWidth(),
contentAlignment = alignment,
) {
val onClick = {
when (cItem.meta.itemStatus) {
is CIStatus.SndErrorAuth -> {
showMsgDeliveryErrorAlert(generalGetString(R.string.message_delivery_error_desc))
}
is CIStatus.SndError -> {
showMsgDeliveryErrorAlert(generalGetString(R.string.unknown_error) + ": ${cItem.meta.itemStatus.agentError}")
}
else -> {}
}
}
Column(
Modifier
.clip(RoundedCornerShape(18.dp))
.combinedClickable(onLongClick = { showMenu.value = true }, onClick = onClick),
) {
@Composable
fun framedItemView() {
FramedItemView(cInfo, cItem, uriHandler, imageProvider, showMember = showMember, linkMode = linkMode, showMenu, receiveFile, onLinkLongClick, scrollToItem)
}
fun deleteMessageQuestionText(): String {
return if (fullDeleteAllowed) {
generalGetString(R.string.delete_message_cannot_be_undone_warning)
} else {
generalGetString(R.string.delete_message_mark_deleted_warning)
}
}
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
fun MsgContentItemDropdownMenu() {
DefaultDropdownMenu(showMenu) {
if (cItem.meta.itemDeleted == null && !live) {
ItemAction(stringResource(R.string.reply_verb), painterResource(R.drawable.ic_reply), onClick = {
if (composeState.value.editing) {
composeState.value = ComposeState(contextItem = ComposeContextItem.QuotedItem(cItem), useLinkPreviews = useLinkPreviews)
} else {
composeState.value = composeState.value.copy(contextItem = ComposeContextItem.QuotedItem(cItem))
}
showMenu.value = false
})
}
ItemAction(stringResource(R.string.share_verb), painterResource(R.drawable.ic_share), onClick = {
val filePath = getLoadedFilePath(SimplexApp.context, cItem.file)
when {
filePath != null -> shareFile(context, cItem.text, filePath)
else -> shareText(context, cItem.content.text)
}
showMenu.value = false
})
ItemAction(stringResource(R.string.copy_verb), painterResource(R.drawable.ic_content_copy), onClick = {
copyText(context, cItem.content.text)
showMenu.value = false
})
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)
if (filePath != null) {
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) {
is MsgContent.MCImage -> {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R || writePermissionState.hasPermission) {
saveImage(context, cItem.file)
} else {
writePermissionState.launchPermissionRequest()
}
}
is MsgContent.MCFile, is MsgContent.MCVoice, is MsgContent.MCVideo -> saveFileLauncher.launch(cItem.file?.fileName)
else -> {}
}
showMenu.value = false
})
}
}
if (cItem.meta.editable && cItem.content.msgContent !is MsgContent.MCVoice && !live) {
ItemAction(stringResource(R.string.edit_verb), painterResource(R.drawable.ic_edit_filled), onClick = {
composeState.value = ComposeState(editingItem = cItem, useLinkPreviews = useLinkPreviews)
showMenu.value = false
})
}
if (cItem.meta.itemDeleted != null && revealed.value) {
ItemAction(
stringResource(R.string.hide_verb),
painterResource(R.drawable.ic_visibility_off),
onClick = {
revealed.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)) {
DeleteItemAction(cItem, showMenu, questionText = deleteMessageQuestionText(), deleteMessage)
}
val groupInfo = cItem.memberToModerate(cInfo)?.first
if (groupInfo != null) {
ModerateItemAction(cItem, questionText = moderateMessageQuestionText(), showMenu, deleteMessage)
}
}
}
@Composable
fun MarkedDeletedItemDropdownMenu() {
DefaultDropdownMenu(showMenu) {
if (!cItem.isDeletedContent) {
ItemAction(
stringResource(R.string.reveal_verb),
painterResource(R.drawable.ic_visibility),
onClick = {
revealed.value = true
showMenu.value = false
}
)
}
DeleteItemAction(cItem, showMenu, questionText = deleteMessageQuestionText(), deleteMessage)
}
}
@Composable
fun ContentItem() {
val mc = cItem.content.msgContent
if (cItem.meta.itemDeleted != null && !revealed.value) {
MarkedDeletedItemView(cItem, cInfo.timedMessagesTTL, showMember = showMember)
MarkedDeletedItemDropdownMenu()
} else if (cItem.quotedItem == null && cItem.meta.itemDeleted == null && !cItem.meta.isLive) {
if (mc is MsgContent.MCText && isShortEmoji(cItem.content.text)) {
EmojiItemView(cItem, cInfo.timedMessagesTTL)
MsgContentItemDropdownMenu()
} else if (mc is MsgContent.MCVoice && cItem.content.text.isEmpty()) {
CIVoiceView(mc.duration, cItem.file, cItem.meta.itemEdited, cItem.chatDir.sent, hasText = false, cItem, cInfo.timedMessagesTTL, longClick = { onLinkLongClick("") })
MsgContentItemDropdownMenu()
} else {
framedItemView()
MsgContentItemDropdownMenu()
}
} else {
framedItemView()
MsgContentItemDropdownMenu()
}
}
@Composable fun DeletedItem() {
DeletedItemView(cItem, cInfo.timedMessagesTTL, showMember = showMember)
DefaultDropdownMenu(showMenu) {
DeleteItemAction(cItem, showMenu, questionText = deleteMessageQuestionText(), deleteMessage)
}
}
@Composable fun CallItem(status: CICallStatus, duration: Int) {
CICallItemView(cInfo, cItem, status, duration, acceptCall)
}
when (val c = cItem.content) {
is CIContent.SndMsgContent -> ContentItem()
is CIContent.RcvMsgContent -> ContentItem()
is CIContent.SndDeleted -> DeletedItem()
is CIContent.RcvDeleted -> DeletedItem()
is CIContent.SndCall -> CallItem(c.status, c.duration)
is CIContent.RcvCall -> CallItem(c.status, c.duration)
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.SndGroupInvitation -> CIGroupInvitationView(cItem, c.groupInvitation, c.memberRole, joinGroup = joinGroup, chatIncognito = cInfo.incognito)
is CIContent.RcvGroupEventContent -> CIEventView(cItem)
is CIContent.SndGroupEventContent -> CIEventView(cItem)
is CIContent.RcvConnEventContent -> CIEventView(cItem)
is CIContent.SndConnEventContent -> CIEventView(cItem)
is CIContent.RcvChatFeature -> CIChatFeatureView(cItem, c.feature, c.enabled.iconColor)
is CIContent.SndChatFeature -> CIChatFeatureView(cItem, c.feature, c.enabled.iconColor)
is CIContent.RcvChatPreference -> {
val ct = if (cInfo is ChatInfo.Direct) cInfo.contact else null
CIFeaturePreferenceView(cItem, ct, c.feature, c.allowed, acceptFeature)
}
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.SndGroupFeature -> CIChatFeatureView(cItem, c.groupFeature, c.preference.enable.iconColor)
is CIContent.RcvChatFeatureRejected -> CIChatFeatureView(cItem, c.feature, 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)
}
}
}
}
@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
fun DeleteItemAction(
cItem: ChatItem,
showMenu: MutableState<Boolean>,
questionText: String,
deleteMessage: (Long, CIDeleteMode) -> Unit
) {
ItemAction(
stringResource(R.string.delete_verb),
painterResource(R.drawable.ic_delete),
onClick = {
showMenu.value = false
deleteMessageAlertDialog(cItem, questionText, deleteMessage = deleteMessage)
},
color = Color.Red
)
}
@Composable
fun ModerateItemAction(
cItem: ChatItem,
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,
modifier = Modifier
.fillMaxWidth()
.weight(1F)
.padding(end = 15.dp),
color = finalColor
)
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) {
AlertManager.shared.showAlertDialogButtons(
title = generalGetString(R.string.delete_message__question),
text = questionText,
buttons = {
Row(
Modifier
.fillMaxWidth()
.padding(horizontal = 8.dp, vertical = 2.dp),
horizontalArrangement = Arrangement.Center,
) {
TextButton(onClick = {
deleteMessage(chatItem.id, CIDeleteMode.cidmInternal)
AlertManager.shared.hideAlert()
}) { Text(stringResource(R.string.for_me_only), color = MaterialTheme.colors.error) }
if (chatItem.meta.editable) {
Spacer(Modifier.padding(horizontal = 4.dp))
TextButton(onClick = {
deleteMessage(chatItem.id, CIDeleteMode.cidmBroadcast)
AlertManager.shared.hideAlert()
}) { 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) {
AlertManager.shared.showAlertMsg(
title = generalGetString(R.string.message_delivery_error_title),
text = description,
)
}
@Preview
@Composable
fun PreviewChatItemView() {
SimpleXTheme {
ChatItemView(
ChatInfo.Direct.sampleData,
ChatItem.getSampleData(
1, CIDirection.DirectSnd(), Clock.System.now(), "hello"
),
useLinkPreviews = true,
linkMode = SimplexLinkMode.DESCRIPTION,
composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = true)) },
deleteMessage = { _, _ -> },
receiveFile = {},
cancelFile = {},
joinGroup = {},
acceptCall = { _ -> },
scrollToItem = {},
acceptFeature = { _, _, _ -> }
)
}
}
@Preview
@Composable
fun PreviewChatItemViewDeletedContent() {
SimpleXTheme {
ChatItemView(
ChatInfo.Direct.sampleData,
ChatItem.getDeletedContentSampleData(),
useLinkPreviews = true,
linkMode = SimplexLinkMode.DESCRIPTION,
composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = true)) },
deleteMessage = { _, _ -> },
receiveFile = {},
cancelFile = {},
joinGroup = {},
acceptCall = { _ -> },
scrollToItem = {},
acceptFeature = { _, _, _ -> }
)
}
}
@@ -14,18 +14,18 @@ import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.graphics.painter.Painter 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 dev.icerock.moko.resources.compose.painterResource import androidx.compose.ui.res.painterResource
import dev.icerock.moko.resources.compose.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.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 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.helpers.* import chat.simplex.app.views.helpers.*
import chat.simplex.res.MR
import kotlinx.datetime.Clock import kotlinx.datetime.Clock
import kotlin.math.min import kotlin.math.min
@@ -119,7 +119,7 @@ fun FramedItemView(
val imageBitmap = base64ToBitmap(qi.content.image).asImageBitmap() val imageBitmap = base64ToBitmap(qi.content.image).asImageBitmap()
Image( Image(
imageBitmap, imageBitmap,
contentDescription = stringResource(MR.strings.image_descr), contentDescription = stringResource(R.string.image_descr),
contentScale = ContentScale.Crop, contentScale = ContentScale.Crop,
modifier = Modifier.size(68.dp).clipToBounds() modifier = Modifier.size(68.dp).clipToBounds()
) )
@@ -131,7 +131,7 @@ fun FramedItemView(
val imageBitmap = base64ToBitmap(qi.content.image).asImageBitmap() val imageBitmap = base64ToBitmap(qi.content.image).asImageBitmap()
Image( Image(
imageBitmap, imageBitmap,
contentDescription = stringResource(MR.strings.video_descr), contentDescription = stringResource(R.string.video_descr),
contentScale = ContentScale.Crop, contentScale = ContentScale.Crop,
modifier = Modifier.size(68.dp).clipToBounds() modifier = Modifier.size(68.dp).clipToBounds()
) )
@@ -141,8 +141,8 @@ fun FramedItemView(
ciQuotedMsgView(qi) ciQuotedMsgView(qi)
} }
Icon( Icon(
if (qi.content is MsgContent.MCFile) painterResource(MR.images.ic_draft_filled) else painterResource(MR.images.ic_mic_filled), 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(MR.strings.icon_descr_file) else stringResource(MR.strings.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)
.size(22.dp), .size(22.dp),
@@ -182,12 +182,12 @@ fun FramedItemView(
PriorityLayout(Modifier, CHAT_IMAGE_LAYOUT_ID) { PriorityLayout(Modifier, CHAT_IMAGE_LAYOUT_ID) {
if (ci.meta.itemDeleted != null) { if (ci.meta.itemDeleted != null) {
if (ci.meta.itemDeleted is CIDeleted.Moderated) { if (ci.meta.itemDeleted is CIDeleted.Moderated) {
FramedItemHeader(String.format(stringResource(MR.strings.moderated_item_description), ci.meta.itemDeleted.byGroupMember.chatViewName), true, painterResource(MR.images.ic_flag)) FramedItemHeader(String.format(stringResource(R.string.moderated_item_description), ci.meta.itemDeleted.byGroupMember.chatViewName), true, painterResource(R.drawable.ic_flag))
} else { } else {
FramedItemHeader(stringResource(MR.strings.marked_deleted_description), true, painterResource(MR.images.ic_delete)) 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(MR.strings.live), false) FramedItemHeader(stringResource(R.string.live), false)
} }
ci.quotedItem?.let { ciQuoteView(it) } ci.quotedItem?.let { ciQuoteView(it) }
if (ci.file == null && ci.formattedText == null && !ci.meta.isLive && isShortEmoji(ci.content.text)) { if (ci.file == null && ci.formattedText == null && !ci.meta.isLive && isShortEmoji(ci.content.text)) {
@@ -221,7 +221,7 @@ fun FramedItemView(
} }
} }
is MsgContent.MCVoice -> { is MsgContent.MCVoice -> {
CIVoiceView(mc.duration, ci.file, ci.meta.itemEdited, ci.chatDir.sent, hasText = true, ci, timedMessagesTTL = chatTTL, longClick = { onLinkLongClick("") }, receiveFile) CIVoiceView(mc.duration, ci.file, ci.meta.itemEdited, ci.chatDir.sent, hasText = true, ci, timedMessagesTTL = chatTTL, longClick = { onLinkLongClick("") })
if (mc.text != "") { if (mc.text != "") {
CIMarkdownText(ci, chatTTL, showMember, linkMode, uriHandler) CIMarkdownText(ci, chatTTL, showMember, linkMode, uriHandler)
} }
@@ -300,7 +300,7 @@ fun PriorityLayout(
// Find important element which should tell what max width other elements can use // Find important element which should tell what max width other elements can use
// Expecting only one such element. Can be less than one but not more // Expecting only one such element. Can be less than one but not more
val imagePlaceable = measureable.firstOrNull { it.layoutId == priorityLayoutId }?.measure(constraints) val imagePlaceable = measureable.firstOrNull { it.layoutId == priorityLayoutId }?.measure(constraints)
val placeables: List<Placeable> = measureable.map { val placeables: List<Placeable> = measureable.fastMap {
if (it.layoutId == priorityLayoutId) if (it.layoutId == priorityLayoutId)
imagePlaceable!! imagePlaceable!!
else else
@@ -19,7 +19,7 @@ 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.* import androidx.compose.ui.platform.*
import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.* import androidx.compose.ui.unit.*
import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.view.isVisible import androidx.core.view.isVisible
@@ -35,7 +35,6 @@ 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.AspectRatioFrameLayout
import com.google.android.exoplayer2.ui.StyledPlayerView import com.google.android.exoplayer2.ui.StyledPlayerView
import chat.simplex.res.MR
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlin.math.absoluteValue import kotlin.math.absoluteValue
@@ -160,7 +159,7 @@ fun ImageFullScreenView(imageProvider: () -> ImageGalleryProvider, close: () ->
placeholder = BitmapPainter(imageBitmap.asImageBitmap()), // show original image while it's still loading by coil placeholder = BitmapPainter(imageBitmap.asImageBitmap()), // show original image while it's still loading by coil
imageLoader = imageLoader imageLoader = imageLoader
), ),
contentDescription = stringResource(MR.strings.image_descr), contentDescription = stringResource(R.string.image_descr),
contentScale = ContentScale.Fit, contentScale = ContentScale.Fit,
modifier = modifier, modifier = modifier,
) )
@@ -179,7 +178,7 @@ fun ImageFullScreenView(imageProvider: () -> ImageGalleryProvider, close: () ->
@Composable @Composable
private fun VideoView(modifier: Modifier, uri: Uri, defaultPreview: Bitmap, currentPage: Boolean) { private fun VideoView(modifier: Modifier, uri: Uri, defaultPreview: Bitmap, currentPage: Boolean) {
val context = LocalContext.current val context = LocalContext.current
val player = remember(uri) { VideoPlayer.getOrCreate(uri, true, defaultPreview, 0L, true) } val player = remember(uri) { VideoPlayer.getOrCreate(uri, true, defaultPreview, 0L, true, context) }
val isCurrentPage = rememberUpdatedState(currentPage) val isCurrentPage = rememberUpdatedState(currentPage)
val play = { val play = {
player.play(true) player.play(true)
@@ -23,7 +23,6 @@ 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
import chat.simplex.res.MR
@Composable @Composable
fun IntegrityErrorItemView(msgError: MsgErrorType, ci: ChatItem, timedMessagesTTL: Int?, showMember: Boolean = false) { fun IntegrityErrorItemView(msgError: MsgErrorType, ci: ChatItem, timedMessagesTTL: Int?, showMember: Boolean = false) {
@@ -31,21 +30,21 @@ fun IntegrityErrorItemView(msgError: MsgErrorType, ci: ChatItem, timedMessagesTT
when (msgError) { when (msgError) {
is MsgErrorType.MsgSkipped -> is MsgErrorType.MsgSkipped ->
AlertManager.shared.showAlertMsg( AlertManager.shared.showAlertMsg(
title = generalGetString(MR.strings.alert_title_skipped_messages), title = generalGetString(R.string.alert_title_skipped_messages),
text = generalGetString(MR.strings.alert_text_skipped_messages_it_can_happen_when) text = generalGetString(R.string.alert_text_skipped_messages_it_can_happen_when)
) )
is MsgErrorType.MsgBadHash -> is MsgErrorType.MsgBadHash ->
AlertManager.shared.showAlertMsg( AlertManager.shared.showAlertMsg(
title = generalGetString(MR.strings.alert_title_msg_bad_hash), title = generalGetString(R.string.alert_title_msg_bad_hash),
text = generalGetString(MR.strings.alert_text_msg_bad_hash) + "\n" + text = generalGetString(R.string.alert_text_msg_bad_hash) + "\n" +
generalGetString(MR.strings.alert_text_fragment_encryption_out_of_sync_old_database) + "\n" + generalGetString(R.string.alert_text_fragment_encryption_out_of_sync_old_database) + "\n" +
generalGetString(MR.strings.alert_text_fragment_please_report_to_developers) generalGetString(R.string.alert_text_fragment_please_report_to_developers)
) )
is MsgErrorType.MsgBadId, is MsgErrorType.MsgDuplicate -> is MsgErrorType.MsgBadId, is MsgErrorType.MsgDuplicate ->
AlertManager.shared.showAlertMsg( AlertManager.shared.showAlertMsg(
title = generalGetString(MR.strings.alert_title_msg_bad_id), title = generalGetString(R.string.alert_title_msg_bad_id),
text = generalGetString(MR.strings.alert_text_msg_bad_id) + "\n" + text = generalGetString(R.string.alert_text_msg_bad_id) + "\n" +
generalGetString(MR.strings.alert_text_fragment_please_report_to_developers) generalGetString(R.string.alert_text_fragment_please_report_to_developers)
) )
} }
} }
@@ -19,8 +19,6 @@ import chat.simplex.app.model.CIDeleted
import chat.simplex.app.model.ChatItem import chat.simplex.app.model.ChatItem
import chat.simplex.app.ui.theme.* import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.helpers.generalGetString import chat.simplex.app.views.helpers.generalGetString
import chat.simplex.res.MR
import kotlinx.datetime.Clock
@Composable @Composable
fun MarkedDeletedItemView(ci: ChatItem, timedMessagesTTL: Int?, showMember: Boolean = false) { fun MarkedDeletedItemView(ci: ChatItem, timedMessagesTTL: Int?, showMember: Boolean = false) {
@@ -36,9 +34,9 @@ fun MarkedDeletedItemView(ci: ChatItem, timedMessagesTTL: Int?, showMember: Bool
) { ) {
Box(Modifier.weight(1f, false)) { Box(Modifier.weight(1f, false)) {
if (ci.meta.itemDeleted is CIDeleted.Moderated) { if (ci.meta.itemDeleted is CIDeleted.Moderated) {
MarkedDeletedText(String.format(generalGetString(MR.strings.moderated_item_description), ci.meta.itemDeleted.byGroupMember.chatViewName)) MarkedDeletedText(String.format(generalGetString(R.string.moderated_item_description), ci.meta.itemDeleted.byGroupMember.chatViewName))
} else { } else {
MarkedDeletedText(generalGetString(MR.strings.marked_deleted_description)) MarkedDeletedText(generalGetString(R.string.marked_deleted_description))
} }
} }
CIMetaView(ci, timedMessagesTTL) CIMetaView(ci, timedMessagesTTL)
@@ -69,7 +67,7 @@ private fun MarkedDeletedText(text: String) {
fun PreviewMarkedDeletedItemView() { fun PreviewMarkedDeletedItemView() {
SimpleXTheme { SimpleXTheme {
DeletedItemView( DeletedItemView(
ChatItem.getSampleData(itemDeleted = CIDeleted.Deleted(Clock.System.now())), ChatItem.getSampleData(itemDeleted = CIDeleted.Deleted()),
null null
) )
} }
@@ -4,11 +4,12 @@ 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 dev.icerock.moko.resources.compose.painterResource import androidx.compose.ui.res.painterResource
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 dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
@@ -17,10 +18,9 @@ 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.onboarding.ReadableTextWithLink 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
import chat.simplex.res.MR
val bold = SpanStyle(fontWeight = FontWeight.Bold) val bold = SpanStyle(fontWeight = FontWeight.Bold)
@@ -29,14 +29,23 @@ fun ChatHelpView(addContact: (() -> Unit)? = null) {
Column( Column(
verticalArrangement = Arrangement.spacedBy(10.dp) verticalArrangement = Arrangement.spacedBy(10.dp)
) { ) {
Text(stringResource(MR.strings.thank_you_for_installing_simplex), lineHeight = 22.sp) val uriHandler = LocalUriHandler.current
ReadableTextWithLink(MR.strings.you_can_connect_to_simplex_chat_founder, simplexTeamUri)
Text(stringResource(R.string.thank_you_for_installing_simplex), lineHeight = 22.sp)
Text(
annotatedStringResource(R.string.you_can_connect_to_simplex_chat_founder),
modifier = Modifier.clickable(onClick = {
uriHandler.openUriCatching(simplexTeamUri)
}),
lineHeight = 22.sp
)
Column( Column(
Modifier.padding(top = 24.dp), Modifier.padding(top = 24.dp),
verticalArrangement = Arrangement.spacedBy(10.dp) verticalArrangement = Arrangement.spacedBy(10.dp)
) { ) {
Text( Text(
stringResource(MR.strings.to_start_a_new_chat_help_header), stringResource(R.string.to_start_a_new_chat_help_header),
style = MaterialTheme.typography.h2, style = MaterialTheme.typography.h2,
lineHeight = 22.sp lineHeight = 22.sp
) )
@@ -44,33 +53,33 @@ fun ChatHelpView(addContact: (() -> Unit)? = null) {
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp) horizontalArrangement = Arrangement.spacedBy(8.dp)
) { ) {
Text(stringResource(MR.strings.chat_help_tap_button)) Text(stringResource(R.string.chat_help_tap_button))
Icon( Icon(
painterResource(MR.images.ic_person_add), painterResource(R.drawable.ic_person_add),
stringResource(MR.strings.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,
) )
Text(stringResource(MR.strings.above_then_preposition_continuation)) Text(stringResource(R.string.above_then_preposition_continuation))
} }
Text(annotatedStringResource(MR.strings.add_new_contact_to_create_one_time_QR_code), lineHeight = 22.sp) Text(annotatedStringResource(R.string.add_new_contact_to_create_one_time_QR_code), lineHeight = 22.sp)
Text(annotatedStringResource(MR.strings.scan_QR_code_to_connect_to_contact_who_shows_QR_code), lineHeight = 22.sp) Text(annotatedStringResource(R.string.scan_QR_code_to_connect_to_contact_who_shows_QR_code), lineHeight = 22.sp)
} }
Column( Column(
Modifier.padding(top = 24.dp), Modifier.padding(top = 24.dp),
verticalArrangement = Arrangement.spacedBy(10.dp) verticalArrangement = Arrangement.spacedBy(10.dp)
) { ) {
Text(stringResource(MR.strings.to_connect_via_link_title), style = MaterialTheme.typography.h2) Text(stringResource(R.string.to_connect_via_link_title), style = MaterialTheme.typography.h2)
Text(stringResource(MR.strings.if_you_received_simplex_invitation_link_you_can_open_in_browser), lineHeight = 22.sp) Text(stringResource(R.string.if_you_received_simplex_invitation_link_you_can_open_in_browser), lineHeight = 22.sp)
Text(annotatedStringResource(MR.strings.desktop_scan_QR_code_from_app_via_scan_QR_code), lineHeight = 22.sp) Text(annotatedStringResource(R.string.desktop_scan_QR_code_from_app_via_scan_QR_code), lineHeight = 22.sp)
Text(annotatedStringResource(MR.strings.mobile_tap_open_in_mobile_app_then_tap_connect_in_app), lineHeight = 22.sp) Text(annotatedStringResource(R.string.mobile_tap_open_in_mobile_app_then_tap_connect_in_app), lineHeight = 22.sp)
} }
Column( Column(
Modifier.padding(vertical = 24.dp), Modifier.padding(vertical = 24.dp),
verticalArrangement = Arrangement.spacedBy(10.dp) verticalArrangement = Arrangement.spacedBy(10.dp)
) { ) {
Text(stringResource(MR.strings.markdown_in_messages), style = MaterialTheme.typography.h2) Text(stringResource(R.string.markdown_in_messages), style = MaterialTheme.typography.h2)
MarkdownHelpView() MarkdownHelpView()
} }
} }
@@ -9,13 +9,14 @@ 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 dev.icerock.moko.resources.compose.painterResource import androidx.compose.ui.res.painterResource
import dev.icerock.moko.resources.compose.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 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.chat.* import chat.simplex.app.views.chat.*
@@ -25,7 +26,6 @@ import chat.simplex.app.views.chat.item.InvalidJSONView
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 chat.simplex.app.views.newchat.ContactConnectionInfoView import chat.simplex.app.views.newchat.ContactConnectionInfoView
import chat.simplex.res.MR
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.datetime.Clock import kotlinx.datetime.Clock
@@ -134,19 +134,8 @@ suspend fun apiFindMessages(chatInfo: ChatInfo, chatModel: ChatModel, search: St
suspend fun setGroupMembers(groupInfo: GroupInfo, chatModel: ChatModel) { suspend fun setGroupMembers(groupInfo: GroupInfo, chatModel: ChatModel) {
val groupMembers = chatModel.controller.apiListMembers(groupInfo.groupId) val groupMembers = chatModel.controller.apiListMembers(groupInfo.groupId)
val currentMembers = chatModel.groupMembers
val newMembers = groupMembers.map { newMember ->
val currentMember = currentMembers.find { it.id == newMember.id }
val currentMemberStats = currentMember?.activeConn?.connectionStats
val newMemberConn = newMember.activeConn
if (currentMemberStats != null && newMemberConn != null && newMemberConn.connectionStats == null) {
newMember.copy(activeConn = newMemberConn.copy(connectionStats = currentMemberStats))
} else {
newMember
}
}
chatModel.groupMembers.clear() chatModel.groupMembers.clear()
chatModel.groupMembers.addAll(newMembers) chatModel.groupMembers.addAll(groupMembers)
} }
@Composable @Composable
@@ -156,7 +145,6 @@ fun ContactMenuItems(chat: Chat, chatModel: ChatModel, showMenu: MutableState<Bo
} else { } else {
MarkUnreadChatAction(chat, chatModel, showMenu) MarkUnreadChatAction(chat, chatModel, showMenu)
} }
ToggleFavoritesChatAction(chat, chatModel, chat.chatInfo.chatSettings?.favorite == true, showMenu)
ToggleNotificationsChatAction(chat, chatModel, chat.chatInfo.ntfsEnabled, showMenu) ToggleNotificationsChatAction(chat, chatModel, chat.chatInfo.ntfsEnabled, showMenu)
ClearChatAction(chat, chatModel, showMenu) ClearChatAction(chat, chatModel, showMenu)
DeleteContactAction(chat, chatModel, showMenu) DeleteContactAction(chat, chatModel, showMenu)
@@ -171,21 +159,12 @@ fun GroupMenuItems(chat: Chat, groupInfo: GroupInfo, chatModel: ChatModel, showM
DeleteGroupAction(chat, groupInfo, chatModel, showMenu) DeleteGroupAction(chat, groupInfo, chatModel, showMenu)
} }
} }
GroupMemberStatus.MemAccepted -> {
if (groupInfo.membership.memberCurrent) {
LeaveGroupAction(groupInfo, chatModel, showMenu)
}
if (groupInfo.canDelete) {
DeleteGroupAction(chat, groupInfo, chatModel, showMenu)
}
}
else -> { else -> {
if (showMarkRead) { if (showMarkRead) {
MarkReadChatAction(chat, chatModel, showMenu) MarkReadChatAction(chat, chatModel, showMenu)
} else { } else {
MarkUnreadChatAction(chat, chatModel, showMenu) MarkUnreadChatAction(chat, chatModel, showMenu)
} }
ToggleFavoritesChatAction(chat, chatModel, chat.chatInfo.chatSettings?.favorite == true, showMenu)
ToggleNotificationsChatAction(chat, chatModel, chat.chatInfo.ntfsEnabled, showMenu) ToggleNotificationsChatAction(chat, chatModel, chat.chatInfo.ntfsEnabled, showMenu)
ClearChatAction(chat, chatModel, showMenu) ClearChatAction(chat, chatModel, showMenu)
if (groupInfo.membership.memberCurrent) { if (groupInfo.membership.memberCurrent) {
@@ -201,8 +180,8 @@ fun GroupMenuItems(chat: Chat, groupInfo: GroupInfo, chatModel: ChatModel, showM
@Composable @Composable
fun MarkReadChatAction(chat: Chat, chatModel: ChatModel, showMenu: MutableState<Boolean>) { fun MarkReadChatAction(chat: Chat, chatModel: ChatModel, showMenu: MutableState<Boolean>) {
ItemAction( ItemAction(
stringResource(MR.strings.mark_read), stringResource(R.string.mark_read),
painterResource(MR.images.ic_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)
@@ -214,8 +193,8 @@ 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>) {
ItemAction( ItemAction(
stringResource(MR.strings.mark_unread), stringResource(R.string.mark_unread),
painterResource(MR.images.ic_mark_chat_unread), painterResource(R.drawable.ic_mark_chat_unread),
onClick = { onClick = {
markChatUnread(chat, chatModel) markChatUnread(chat, chatModel)
showMenu.value = false showMenu.value = false
@@ -223,25 +202,13 @@ fun MarkUnreadChatAction(chat: Chat, chatModel: ChatModel, showMenu: MutableStat
) )
} }
@Composable
fun ToggleFavoritesChatAction(chat: Chat, chatModel: ChatModel, favorite: Boolean, showMenu: MutableState<Boolean>) {
ItemAction(
if (favorite) stringResource(MR.strings.unfavorite_chat) else stringResource(MR.strings.favorite_chat),
if (favorite) painterResource(MR.images.ic_star_off) else painterResource(MR.images.ic_star),
onClick = {
toggleChatFavorite(chat, !favorite, chatModel)
showMenu.value = false
}
)
}
@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(MR.strings.mute_chat) else stringResource(MR.strings.unmute_chat), if (ntfsEnabled) stringResource(R.string.mute_chat) else stringResource(R.string.unmute_chat),
if (ntfsEnabled) painterResource(MR.images.ic_notifications_off) else painterResource(MR.images.ic_notifications), if (ntfsEnabled) painterResource(R.drawable.ic_notifications_off) else painterResource(R.drawable.ic_notifications),
onClick = { onClick = {
toggleNotifications(chat, !ntfsEnabled, chatModel) changeNtfsStatePerChat(!ntfsEnabled, mutableStateOf(ntfsEnabled), chat, chatModel)
showMenu.value = false showMenu.value = false
} }
) )
@@ -250,8 +217,8 @@ fun ToggleNotificationsChatAction(chat: Chat, chatModel: ChatModel, ntfsEnabled:
@Composable @Composable
fun ClearChatAction(chat: Chat, chatModel: ChatModel, showMenu: MutableState<Boolean>) { fun ClearChatAction(chat: Chat, chatModel: ChatModel, showMenu: MutableState<Boolean>) {
ItemAction( ItemAction(
stringResource(MR.strings.clear_chat_menu_action), stringResource(R.string.clear_chat_menu_action),
painterResource(MR.images.ic_settings_backup_restore), painterResource(R.drawable.ic_settings_backup_restore),
onClick = { onClick = {
clearChatDialog(chat.chatInfo, chatModel) clearChatDialog(chat.chatInfo, chatModel)
showMenu.value = false showMenu.value = false
@@ -263,8 +230,8 @@ fun ClearChatAction(chat: Chat, chatModel: ChatModel, showMenu: MutableState<Boo
@Composable @Composable
fun DeleteContactAction(chat: Chat, chatModel: ChatModel, showMenu: MutableState<Boolean>) { fun DeleteContactAction(chat: Chat, chatModel: ChatModel, showMenu: MutableState<Boolean>) {
ItemAction( ItemAction(
stringResource(MR.strings.delete_contact_menu_action), stringResource(R.string.delete_contact_menu_action),
painterResource(MR.images.ic_delete), painterResource(R.drawable.ic_delete),
onClick = { onClick = {
deleteContactDialog(chat.chatInfo, chatModel) deleteContactDialog(chat.chatInfo, chatModel)
showMenu.value = false showMenu.value = false
@@ -276,8 +243,8 @@ fun DeleteContactAction(chat: Chat, chatModel: ChatModel, showMenu: MutableState
@Composable @Composable
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(MR.strings.delete_group_menu_action), stringResource(R.string.delete_group_menu_action),
painterResource(MR.images.ic_delete), painterResource(R.drawable.ic_delete),
onClick = { onClick = {
deleteGroupDialog(chat.chatInfo, groupInfo, chatModel) deleteGroupDialog(chat.chatInfo, groupInfo, chatModel)
showMenu.value = false showMenu.value = false
@@ -290,8 +257,8 @@ fun DeleteGroupAction(chat: Chat, groupInfo: GroupInfo, chatModel: ChatModel, sh
fun JoinGroupAction(chat: Chat, groupInfo: GroupInfo, chatModel: ChatModel, showMenu: MutableState<Boolean>) { fun JoinGroupAction(chat: Chat, groupInfo: GroupInfo, chatModel: ChatModel, showMenu: MutableState<Boolean>) {
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(MR.strings.join_group_incognito_button) else stringResource(MR.strings.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) painterResource(MR.images.ic_theater_comedy_filled) else painterResource(MR.images.ic_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()
@@ -303,8 +270,8 @@ fun JoinGroupAction(chat: Chat, groupInfo: GroupInfo, chatModel: ChatModel, show
@Composable @Composable
fun LeaveGroupAction(groupInfo: GroupInfo, chatModel: ChatModel, showMenu: MutableState<Boolean>) { fun LeaveGroupAction(groupInfo: GroupInfo, chatModel: ChatModel, showMenu: MutableState<Boolean>) {
ItemAction( ItemAction(
stringResource(MR.strings.leave_group_button), stringResource(R.string.leave_group_button),
painterResource(MR.images.ic_logout), painterResource(R.drawable.ic_logout),
onClick = { onClick = {
leaveGroupDialog(groupInfo, chatModel) leaveGroupDialog(groupInfo, chatModel)
showMenu.value = false showMenu.value = false
@@ -316,8 +283,8 @@ fun LeaveGroupAction(groupInfo: GroupInfo, chatModel: ChatModel, showMenu: Mutab
@Composable @Composable
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(MR.strings.accept_contact_incognito_button) else stringResource(MR.strings.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) painterResource(MR.images.ic_theater_comedy_filled) else painterResource(MR.images.ic_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.apiId, chatInfo, true, chatModel) acceptContactRequest(chatInfo.apiId, chatInfo, true, chatModel)
@@ -325,8 +292,8 @@ fun ContactRequestMenuItems(chatInfo: ChatInfo.ContactRequest, chatModel: ChatMo
} }
) )
ItemAction( ItemAction(
stringResource(MR.strings.reject_contact_button), stringResource(R.string.reject_contact_button),
painterResource(MR.images.ic_close), painterResource(R.drawable.ic_close),
onClick = { onClick = {
rejectContactRequest(chatInfo, chatModel) rejectContactRequest(chatInfo, chatModel)
showMenu.value = false showMenu.value = false
@@ -338,8 +305,8 @@ fun ContactRequestMenuItems(chatInfo: ChatInfo.ContactRequest, chatModel: ChatMo
@Composable @Composable
fun ContactConnectionMenuItems(chatInfo: ChatInfo.ContactConnection, chatModel: ChatModel, showMenu: MutableState<Boolean>) { fun ContactConnectionMenuItems(chatInfo: ChatInfo.ContactConnection, chatModel: ChatModel, showMenu: MutableState<Boolean>) {
ItemAction( ItemAction(
stringResource(MR.strings.set_contact_name), stringResource(R.string.set_contact_name),
painterResource(MR.images.ic_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)
@@ -348,8 +315,8 @@ fun ContactConnectionMenuItems(chatInfo: ChatInfo.ContactConnection, chatModel:
}, },
) )
ItemAction( ItemAction(
stringResource(MR.strings.delete_verb), stringResource(R.string.delete_verb),
painterResource(MR.images.ic_delete), painterResource(R.drawable.ic_delete),
onClick = { onClick = {
deleteContactConnectionAlert(chatInfo.contactConnection, chatModel) {} deleteContactConnectionAlert(chatInfo.contactConnection, chatModel) {}
showMenu.value = false showMenu.value = false
@@ -361,14 +328,14 @@ fun ContactConnectionMenuItems(chatInfo: ChatInfo.ContactConnection, chatModel:
@Composable @Composable
private fun InvalidDataView() { private fun InvalidDataView() {
Row { Row {
ProfileImage(72.dp, null, MR.images.ic_account_circle_filled, MaterialTheme.colors.secondary) 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)
.weight(1F) .weight(1F)
) { ) {
Text( Text(
stringResource(MR.strings.invalid_data), stringResource(R.string.invalid_data),
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.h3, style = MaterialTheme.typography.h3,
@@ -425,11 +392,11 @@ fun markChatUnread(chat: Chat, chatModel: ChatModel) {
fun contactRequestAlertDialog(contactRequest: ChatInfo.ContactRequest, chatModel: ChatModel) { fun contactRequestAlertDialog(contactRequest: ChatInfo.ContactRequest, chatModel: ChatModel) {
AlertManager.shared.showAlertDialog( AlertManager.shared.showAlertDialog(
title = generalGetString(MR.strings.accept_connection_request__question), title = generalGetString(R.string.accept_connection_request__question),
text = generalGetString(MR.strings.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(MR.strings.accept_contact_incognito_button) else generalGetString(MR.strings.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.apiId, contactRequest, true, chatModel) }, onConfirm = { acceptContactRequest(contactRequest.apiId, contactRequest, true, chatModel) },
dismissText = generalGetString(MR.strings.reject_contact_button), dismissText = generalGetString(R.string.reject_contact_button),
onDismiss = { rejectContactRequest(contactRequest, chatModel) } onDismiss = { rejectContactRequest(contactRequest, chatModel) }
) )
} }
@@ -454,12 +421,12 @@ fun rejectContactRequest(contactRequest: ChatInfo.ContactRequest, chatModel: Cha
fun contactConnectionAlertDialog(connection: PendingContactConnection, chatModel: ChatModel) { fun contactConnectionAlertDialog(connection: PendingContactConnection, chatModel: ChatModel) {
AlertManager.shared.showAlertDialogButtons( AlertManager.shared.showAlertDialogButtons(
title = generalGetString( title = generalGetString(
if (connection.initiated) MR.strings.you_invited_your_contact if (connection.initiated) R.string.you_invited_your_contact
else MR.strings.you_accepted_connection else R.string.you_accepted_connection
), ),
text = generalGetString( text = generalGetString(
if (connection.viaContactUri) MR.strings.you_will_be_connected_when_your_connection_request_is_accepted if (connection.viaContactUri) R.string.you_will_be_connected_when_your_connection_request_is_accepted
else MR.strings.you_will_be_connected_when_your_contacts_device_is_online else R.string.you_will_be_connected_when_your_contacts_device_is_online
), ),
buttons = { buttons = {
Row( Row(
@@ -472,11 +439,11 @@ fun contactConnectionAlertDialog(connection: PendingContactConnection, chatModel
AlertManager.shared.hideAlert() AlertManager.shared.hideAlert()
deleteContactConnectionAlert(connection, chatModel) {} deleteContactConnectionAlert(connection, chatModel) {}
}) { }) {
Text(stringResource(MR.strings.delete_verb)) Text(stringResource(R.string.delete_verb))
} }
Spacer(Modifier.padding(horizontal = 4.dp)) Spacer(Modifier.padding(horizontal = 4.dp))
TextButton(onClick = { AlertManager.shared.hideAlert() }) { TextButton(onClick = { AlertManager.shared.hideAlert() }) {
Text(stringResource(MR.strings.ok)) Text(stringResource(R.string.ok))
} }
} }
} }
@@ -485,12 +452,12 @@ fun contactConnectionAlertDialog(connection: PendingContactConnection, chatModel
fun deleteContactConnectionAlert(connection: PendingContactConnection, chatModel: ChatModel, onSuccess: () -> Unit) { fun deleteContactConnectionAlert(connection: PendingContactConnection, chatModel: ChatModel, onSuccess: () -> Unit) {
AlertManager.shared.showAlertDialog( AlertManager.shared.showAlertDialog(
title = generalGetString(MR.strings.delete_pending_connection__question), title = generalGetString(R.string.delete_pending_connection__question),
text = generalGetString( text = generalGetString(
if (connection.initiated) MR.strings.contact_you_shared_link_with_wont_be_able_to_connect if (connection.initiated) R.string.contact_you_shared_link_with_wont_be_able_to_connect
else MR.strings.connection_you_accepted_will_be_cancelled else R.string.connection_you_accepted_will_be_cancelled
), ),
confirmText = generalGetString(MR.strings.delete_verb), confirmText = generalGetString(R.string.delete_verb),
onConfirm = { onConfirm = {
withApi { withApi {
AlertManager.shared.hideAlert() AlertManager.shared.hideAlert()
@@ -506,9 +473,9 @@ fun deleteContactConnectionAlert(connection: PendingContactConnection, chatModel
fun pendingContactAlertDialog(chatInfo: ChatInfo, chatModel: ChatModel) { fun pendingContactAlertDialog(chatInfo: ChatInfo, chatModel: ChatModel) {
AlertManager.shared.showAlertDialog( AlertManager.shared.showAlertDialog(
title = generalGetString(MR.strings.alert_title_contact_connection_pending), title = generalGetString(R.string.alert_title_contact_connection_pending),
text = generalGetString(MR.strings.alert_text_connection_pending_they_need_to_be_online_can_delete_and_retry), text = generalGetString(R.string.alert_text_connection_pending_they_need_to_be_online_can_delete_and_retry),
confirmText = generalGetString(MR.strings.button_delete_contact), confirmText = generalGetString(R.string.button_delete_contact),
onConfirm = { onConfirm = {
withApi { withApi {
val r = chatModel.controller.apiDeleteChat(chatInfo.chatType, chatInfo.apiId) val r = chatModel.controller.apiDeleteChat(chatInfo.chatType, chatInfo.apiId)
@@ -519,26 +486,26 @@ fun pendingContactAlertDialog(chatInfo: ChatInfo, chatModel: ChatModel) {
} }
}, },
destructive = true, destructive = true,
dismissText = generalGetString(MR.strings.cancel_verb), dismissText = generalGetString(R.string.cancel_verb),
) )
} }
fun acceptGroupInvitationAlertDialog(groupInfo: GroupInfo, chatModel: ChatModel) { fun acceptGroupInvitationAlertDialog(groupInfo: GroupInfo, chatModel: ChatModel) {
AlertManager.shared.showAlertDialog( AlertManager.shared.showAlertDialog(
title = generalGetString(MR.strings.join_group_question), title = generalGetString(R.string.join_group_question),
text = generalGetString(MR.strings.you_are_invited_to_group_join_to_connect_with_group_members), text = generalGetString(R.string.you_are_invited_to_group_join_to_connect_with_group_members),
confirmText = if (groupInfo.membership.memberIncognito) generalGetString(MR.strings.join_group_incognito_button) else generalGetString(MR.strings.join_group_button), confirmText = if (groupInfo.membership.memberIncognito) generalGetString(R.string.join_group_incognito_button) else generalGetString(R.string.join_group_button),
onConfirm = { withApi { chatModel.controller.apiJoinGroup(groupInfo.groupId) } }, onConfirm = { withApi { chatModel.controller.apiJoinGroup(groupInfo.groupId) } },
dismissText = generalGetString(MR.strings.delete_verb), dismissText = generalGetString(R.string.delete_verb),
onDismiss = { deleteGroup(groupInfo, chatModel) } onDismiss = { deleteGroup(groupInfo, chatModel) }
) )
} }
fun cantInviteIncognitoAlert() { fun cantInviteIncognitoAlert() {
AlertManager.shared.showAlertMsg( AlertManager.shared.showAlertMsg(
title = generalGetString(MR.strings.alert_title_cant_invite_contacts), title = generalGetString(R.string.alert_title_cant_invite_contacts),
text = generalGetString(MR.strings.alert_title_cant_invite_contacts_descr), text = generalGetString(R.string.alert_title_cant_invite_contacts_descr),
confirmText = generalGetString(MR.strings.ok), confirmText = generalGetString(R.string.ok),
) )
} }
@@ -555,28 +522,18 @@ fun deleteGroup(groupInfo: GroupInfo, chatModel: ChatModel) {
fun groupInvitationAcceptedAlert() { fun groupInvitationAcceptedAlert() {
AlertManager.shared.showAlertMsg( AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.joining_group), generalGetString(R.string.joining_group),
generalGetString(MR.strings.youve_accepted_group_invitation_connecting_to_inviting_group_member) generalGetString(R.string.youve_accepted_group_invitation_connecting_to_inviting_group_member)
) )
} }
fun toggleNotifications(chat: Chat, enableNtfs: Boolean, chatModel: ChatModel, currentState: MutableState<Boolean>? = null) { fun changeNtfsStatePerChat(enabled: Boolean, currentState: MutableState<Boolean>, chat: Chat, chatModel: ChatModel) {
val chatSettings = (chat.chatInfo.chatSettings ?: ChatSettings.defaults).copy(enableNtfs = enableNtfs)
updateChatSettings(chat, chatSettings, chatModel, currentState)
}
fun toggleChatFavorite(chat: Chat, favorite: Boolean, chatModel: ChatModel) {
val chatSettings = (chat.chatInfo.chatSettings ?: ChatSettings.defaults).copy(favorite = favorite)
updateChatSettings(chat, chatSettings, chatModel)
}
fun updateChatSettings(chat: Chat, chatSettings: ChatSettings, chatModel: ChatModel, currentState: MutableState<Boolean>? = null) {
val newChatInfo = when(chat.chatInfo) { val newChatInfo = when(chat.chatInfo) {
is ChatInfo.Direct -> with (chat.chatInfo) { is ChatInfo.Direct -> with (chat.chatInfo) {
ChatInfo.Direct(contact.copy(chatSettings = chatSettings)) ChatInfo.Direct(contact.copy(chatSettings = contact.chatSettings.copy(enableNtfs = enabled)))
} }
is ChatInfo.Group -> with(chat.chatInfo) { is ChatInfo.Group -> with(chat.chatInfo) {
ChatInfo.Group(groupInfo.copy(chatSettings = chatSettings)) ChatInfo.Group(groupInfo.copy(chatSettings = groupInfo.chatSettings.copy(enableNtfs = enabled)))
} }
else -> null else -> null
} }
@@ -592,13 +549,10 @@ fun updateChatSettings(chat: Chat, chatSettings: ChatSettings, chatModel: ChatMo
} }
if (res && newChatInfo != null) { if (res && newChatInfo != null) {
chatModel.updateChatInfo(newChatInfo) chatModel.updateChatInfo(newChatInfo)
if (!chatSettings.enableNtfs) { if (!enabled) {
chatModel.controller.ntfManager.cancelNotificationsForChat(chat.id) chatModel.controller.ntfManager.cancelNotificationsForChat(chat.id)
} }
val current = currentState?.value currentState.value = enabled
if (current != null) {
currentState.value = !current
}
} }
} }
} }
@@ -13,13 +13,15 @@ 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.platform.LocalUriHandler import androidx.compose.ui.platform.LocalUriHandler
import dev.icerock.moko.resources.compose.painterResource import androidx.compose.ui.res.painterResource
import dev.icerock.moko.resources.compose.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.* import androidx.compose.ui.unit.*
import androidx.fragment.app.FragmentActivity
import chat.simplex.app.* import chat.simplex.app.*
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.*
@@ -28,13 +30,12 @@ import chat.simplex.app.views.onboarding.WhatsNewView
import chat.simplex.app.views.onboarding.shouldShowWhatsNew import chat.simplex.app.views.onboarding.shouldShowWhatsNew
import chat.simplex.app.views.usersettings.SettingsView import chat.simplex.app.views.usersettings.SettingsView
import chat.simplex.app.views.usersettings.simplexTeamUri import chat.simplex.app.views.usersettings.simplexTeamUri
import chat.simplex.res.MR
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow 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 = AnimatedViewState.saver()) { mutableStateOf(MutableStateFlow(AnimatedViewState.GONE)) } val newChatSheetState by rememberSaveable(stateSaver = AnimatedViewState.saver()) { mutableStateOf(MutableStateFlow(AnimatedViewState.GONE)) }
val userPickerState by rememberSaveable(stateSaver = AnimatedViewState.saver()) { mutableStateOf(MutableStateFlow(AnimatedViewState.GONE)) } val userPickerState by rememberSaveable(stateSaver = AnimatedViewState.saver()) { mutableStateOf(MutableStateFlow(AnimatedViewState.GONE)) }
val showNewChatSheet = { val showNewChatSheet = {
@@ -86,7 +87,7 @@ fun ChatListView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit, stopped:
backgroundColor = if (!stopped) MaterialTheme.colors.primary else MaterialTheme.colors.secondary, backgroundColor = if (!stopped) MaterialTheme.colors.primary else MaterialTheme.colors.secondary,
contentColor = Color.White contentColor = Color.White
) { ) {
Icon(if (!newChatSheetState.collectAsState().value.isVisible()) painterResource(MR.images.ic_edit_filled) else painterResource(MR.images.ic_close), stringResource(MR.strings.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))
} }
} }
} }
@@ -103,7 +104,7 @@ fun ChatListView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit, stopped:
if (!stopped && !newChatSheetState.collectAsState().value.isVisible()) { if (!stopped && !newChatSheetState.collectAsState().value.isVisible()) {
OnboardingButtons(showNewChatSheet) OnboardingButtons(showNewChatSheet)
} }
Text(stringResource(MR.strings.you_have_no_chats), Modifier.align(Alignment.Center), color = MaterialTheme.colors.secondary) Text(stringResource(R.string.you_have_no_chats), Modifier.align(Alignment.Center), color = MaterialTheme.colors.secondary)
} }
} }
} }
@@ -129,11 +130,11 @@ fun ChatListView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit, stopped:
private fun OnboardingButtons(openNewChatSheet: () -> Unit) { 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(MR.strings.chat_with_developers)) { ConnectButton(generalGetString(R.string.chat_with_developers)) {
uriHandler.openUriCatching(simplexTeamUri) uriHandler.openUriCatching(simplexTeamUri)
} }
Spacer(Modifier.height(DEFAULT_PADDING)) Spacer(Modifier.height(DEFAULT_PADDING))
ConnectButton(generalGetString(MR.strings.tap_to_start_new_chat), openNewChatSheet) ConnectButton(generalGetString(R.string.tap_to_start_new_chat), openNewChatSheet)
val color = MaterialTheme.colors.primaryVariant 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 {
@@ -175,10 +176,10 @@ private fun ChatListToolbar(chatModel: ChatModel, drawerState: DrawerState, user
BackHandler(onBack = hideSearchOnBack) BackHandler(onBack = hideSearchOnBack)
} }
val barButtons = arrayListOf<@Composable RowScope.() -> Unit>() val barButtons = arrayListOf<@Composable RowScope.() -> Unit>()
if (chatModel.chats.size > 0) { if (chatModel.chats.size >= 8) {
barButtons.add { barButtons.add {
IconButton({ showSearch = true }) { IconButton({ showSearch = true }) {
Icon(painterResource(MR.images.ic_search_500), stringResource(MR.strings.search_verb).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)
} }
} }
} }
@@ -186,13 +187,13 @@ private fun ChatListToolbar(chatModel: ChatModel, drawerState: DrawerState, user
barButtons.add { barButtons.add {
IconButton(onClick = { IconButton(onClick = {
AlertManager.shared.showAlertMsg( AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.chat_is_stopped_indication), generalGetString(R.string.chat_is_stopped_indication),
generalGetString(MR.strings.you_can_start_chat_via_setting_or_by_restarting_the_app) generalGetString(R.string.you_can_start_chat_via_setting_or_by_restarting_the_app)
) )
}) { }) {
Icon( Icon(
painterResource(MR.images.ic_report_filled), painterResource(R.drawable.ic_report_filled),
generalGetString(MR.strings.chat_is_stopped_indication), generalGetString(R.string.chat_is_stopped_indication),
tint = Color.Red, tint = Color.Red,
) )
} }
@@ -221,21 +222,18 @@ private fun ChatListToolbar(chatModel: ChatModel, drawerState: DrawerState, user
}, },
title = { title = {
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
if (chatModel.incognito.value) {
Icon(
painterResource(MR.images.ic_theater_comedy_filled),
stringResource(MR.strings.incognito),
tint = Indigo,
modifier = Modifier.padding(10.dp).size(26.dp)
)
}
Text( Text(
stringResource(MR.strings.your_chats), stringResource(R.string.your_chats),
color = MaterialTheme.colors.onBackground, color = MaterialTheme.colors.onBackground,
fontWeight = FontWeight.SemiBold, fontWeight = FontWeight.SemiBold,
) )
if (chatModel.chats.size > 0) { if (chatModel.incognito.value) {
ToggleFilterButton() Icon(
painterResource(R.drawable.ic_theater_comedy_filled),
stringResource(R.string.incognito),
tint = Indigo,
modifier = Modifier.padding(10.dp).size(26.dp)
)
} }
} }
}, },
@@ -277,24 +275,6 @@ private fun BoxScope.unreadBadge(text: String? = "") {
) )
} }
@Composable
private fun ToggleFilterButton() {
val pref = remember { SimplexApp.context.chatModel.controller.appPrefs.showUnreadAndFavorites }
IconButton(onClick = { pref.set(!pref.get()) }) {
Icon(
painterResource(MR.images.ic_filter_list),
null,
tint = if (pref.state.value) MaterialTheme.colors.background else MaterialTheme.colors.primary,
modifier = Modifier
.padding(3.dp)
.background(color = if (pref.state.value) MaterialTheme.colors.primary else MaterialTheme.colors.background, shape = RoundedCornerShape(50))
.border(width = 1.dp, color = MaterialTheme.colors.primary, shape = RoundedCornerShape(50))
.padding(3.dp)
.size(16.dp)
)
}
}
@Composable @Composable
private fun ProgressIndicator() { private fun ProgressIndicator() {
CircularProgressIndicator( CircularProgressIndicator(
@@ -310,12 +290,14 @@ private var lazyListState = 0 to 0
@Composable @Composable
private fun ChatList(chatModel: ChatModel, search: String) { private fun ChatList(chatModel: ChatModel, search: String) {
val filter: (Chat) -> Boolean = { chat: Chat ->
chat.chatInfo.chatViewName.lowercase().contains(search.lowercase())
}
val listState = rememberLazyListState(lazyListState.first, lazyListState.second) val listState = rememberLazyListState(lazyListState.first, lazyListState.second)
DisposableEffect(Unit) { DisposableEffect(Unit) {
onDispose { lazyListState = listState.firstVisibleItemIndex to listState.firstVisibleItemScrollOffset } onDispose { lazyListState = listState.firstVisibleItemIndex to listState.firstVisibleItemScrollOffset }
} }
val showUnreadAndFavorites = remember { chatModel.controller.appPrefs.showUnreadAndFavorites.state }.value val chats by remember(search) { derivedStateOf { if (search.isEmpty()) chatModel.chats else chatModel.chats.filter(filter) } }
val chats by remember(search, showUnreadAndFavorites) { derivedStateOf { filteredChats(showUnreadAndFavorites, search) } }
LazyColumn( LazyColumn(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
listState listState
@@ -324,44 +306,4 @@ private fun ChatList(chatModel: ChatModel, search: String) {
ChatListNavLinkView(chat, chatModel) ChatListNavLinkView(chat, chatModel)
} }
} }
if (chats.isEmpty() && !chatModel.chats.isEmpty()) {
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Text(generalGetString(MR.strings.no_filtered_chats), color = MaterialTheme.colors.secondary)
}
}
} }
private fun filteredChats(showUnreadAndFavorites: Boolean, searchText: String): List<Chat> {
val chatModel = SimplexApp.context.chatModel
val s = searchText.trim().lowercase()
return if (s.isEmpty() && !showUnreadAndFavorites)
chatModel.chats
else {
chatModel.chats.filter { chat ->
when (val cInfo = chat.chatInfo) {
is ChatInfo.Direct -> if (s.isEmpty()) {
filtered(chat)
} else {
(viewNameContains(cInfo, s) ||
cInfo.contact.profile.displayName.lowercase().contains(s) ||
cInfo.contact.fullName.lowercase().contains(s))
}
is ChatInfo.Group -> if (s.isEmpty()) {
(filtered(chat) || cInfo.groupInfo.membership.memberStatus == GroupMemberStatus.MemInvited)
} else {
viewNameContains(cInfo, s)
}
is ChatInfo.ContactRequest -> s.isEmpty() || viewNameContains(cInfo, s)
is ChatInfo.ContactConnection -> s.isNotEmpty() && cInfo.contactConnection.localAlias.lowercase().contains(s)
is ChatInfo.InvalidJSON -> false
}
}
}
}
private fun filtered(chat: Chat): Boolean =
(chat.chatInfo.chatSettings?.favorite ?: false) || chat.chatStats.unreadCount > 0 || chat.chatStats.unreadChat
private fun viewNameContains(cInfo: ChatInfo, s: String): Boolean =
cInfo.chatViewName.lowercase().contains(s.lowercase())
@@ -12,8 +12,8 @@ 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 dev.icerock.moko.resources.compose.painterResource import androidx.compose.ui.res.painterResource
import dev.icerock.moko.resources.compose.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.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
@@ -26,8 +26,6 @@ import chat.simplex.app.views.chat.ComposePreview
import chat.simplex.app.views.chat.ComposeState 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.*
import chat.simplex.res.MR
import dev.icerock.moko.resources.ImageResource
@Composable @Composable
fun ChatPreviewView( fun ChatPreviewView(
@@ -45,8 +43,8 @@ fun ChatPreviewView(
@Composable @Composable
fun groupInactiveIcon() { fun groupInactiveIcon() {
Icon( Icon(
painterResource(MR.images.ic_cancel_filled), painterResource(R.drawable.ic_cancel_filled),
stringResource(MR.strings.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 = MaterialTheme.colors.secondary tint = MaterialTheme.colors.secondary
) )
@@ -78,15 +76,15 @@ fun ChatPreviewView(
@Composable @Composable
fun VerifiedIcon() { fun VerifiedIcon() {
Icon(painterResource(MR.images.ic_verified_user), null, Modifier.size(19.dp).padding(end = 3.dp, top = 1.dp), tint = MaterialTheme.colors.secondary) 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 messageDraft(draft: ComposeState): Pair<AnnotatedString, Map<String, InlineTextContent>> {
fun attachment(): Pair<ImageResource, String?>? = fun attachment(): Pair<Int, String?>? =
when (draft.preview) { when (draft.preview) {
is ComposePreview.FilePreview -> MR.images.ic_draft_filled to draft.preview.fileName is ComposePreview.FilePreview -> R.drawable.ic_draft_filled to draft.preview.fileName
is ComposePreview.MediaPreview -> MR.images.ic_image to null is ComposePreview.MediaPreview -> R.drawable.ic_image to null
is ComposePreview.VoicePreview -> MR.images.ic_play_arrow_filled to durationText(draft.preview.durationMs / 1000) is ComposePreview.VoicePreview -> R.drawable.ic_play_arrow_filled to durationText(draft.preview.durationMs / 1000)
else -> null else -> null
} }
@@ -107,12 +105,12 @@ fun ChatPreviewView(
"editIcon" to InlineTextContent( "editIcon" to InlineTextContent(
Placeholder(20.sp, 20.sp, PlaceholderVerticalAlign.TextCenter) Placeholder(20.sp, 20.sp, PlaceholderVerticalAlign.TextCenter)
) { ) {
Icon(painterResource(MR.images.ic_edit_note), null, tint = MaterialTheme.colors.primary) Icon(painterResource(R.drawable.ic_edit_note), null, tint = MaterialTheme.colors.primary)
}, },
"attachmentIcon" to InlineTextContent( "attachmentIcon" to InlineTextContent(
Placeholder(20.sp, 20.sp, PlaceholderVerticalAlign.TextCenter) Placeholder(20.sp, 20.sp, PlaceholderVerticalAlign.TextCenter)
) { ) {
Icon(if (attachment?.first != null) painterResource(attachment.first) else painterResource(MR.images.ic_edit_note), null, tint = MaterialTheme.colors.secondary) Icon(if (attachment?.first != null) painterResource(attachment.first) else painterResource(R.drawable.ic_edit_note), null, tint = MaterialTheme.colors.secondary)
} }
) )
return text to inlineContent return text to inlineContent
@@ -145,7 +143,7 @@ fun ChatPreviewView(
val (text: CharSequence, inlineTextContent) = when { val (text: CharSequence, inlineTextContent) = when {
chatModelDraftChatId == chat.id && chatModelDraft != null -> remember(chatModelDraft) { messageDraft(chatModelDraft) } chatModelDraftChatId == chat.id && chatModelDraft != null -> remember(chatModelDraft) { messageDraft(chatModelDraft) }
ci.meta.itemDeleted == null -> ci.text to null ci.meta.itemDeleted == null -> ci.text to null
else -> generalGetString(MR.strings.marked_deleted_description) to null else -> generalGetString(R.string.marked_deleted_description) to null
} }
val formattedText = when { val formattedText = when {
chatModelDraftChatId == chat.id && chatModelDraft != null -> null chatModelDraftChatId == chat.id && chatModelDraft != null -> null
@@ -172,12 +170,12 @@ fun ChatPreviewView(
when (cInfo) { when (cInfo) {
is ChatInfo.Direct -> is ChatInfo.Direct ->
if (!cInfo.ready) { if (!cInfo.ready) {
Text(stringResource(MR.strings.contact_connection_pending), color = MaterialTheme.colors.secondary) 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(MR.strings.group_connection_pending), color = MaterialTheme.colors.secondary) GroupMemberStatus.MemAccepted -> Text(stringResource(R.string.group_connection_pending), color = MaterialTheme.colors.secondary)
else -> {} else -> {}
} }
else -> {} else -> {}
@@ -238,23 +236,8 @@ fun ChatPreviewView(
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
Icon( Icon(
painterResource(MR.images.ic_notifications_off_filled), painterResource(R.drawable.ic_notifications_off_filled),
contentDescription = generalGetString(MR.strings.notifications), contentDescription = generalGetString(R.string.notifications),
tint = MaterialTheme.colors.secondary,
modifier = Modifier
.padding(horizontal = 3.dp)
.padding(vertical = 1.dp)
.size(17.dp)
)
}
} else if (chat.chatInfo.chatSettings?.favorite == true) {
Box(
Modifier.padding(top = 24.dp),
contentAlignment = Alignment.Center
) {
Icon(
painterResource(MR.images.ic_star_filled),
contentDescription = generalGetString(MR.strings.favorite_chat),
tint = MaterialTheme.colors.secondary, tint = MaterialTheme.colors.secondary,
modifier = Modifier modifier = Modifier
.padding(horizontal = 3.dp) .padding(horizontal = 3.dp)
@@ -278,16 +261,16 @@ fun ChatPreviewView(
@Composable @Composable
private fun groupInvitationPreviewText(chatModelIncognito: Boolean, currentUserProfileDisplayName: String?, groupInfo: GroupInfo): String { private fun groupInvitationPreviewText(chatModelIncognito: Boolean, currentUserProfileDisplayName: String?, groupInfo: GroupInfo): String {
return if (groupInfo.membership.memberIncognito) return if (groupInfo.membership.memberIncognito)
String.format(stringResource(MR.strings.group_preview_join_as), groupInfo.membership.memberProfile.displayName) String.format(stringResource(R.string.group_preview_join_as), groupInfo.membership.memberProfile.displayName)
else if (chatModelIncognito) else if (chatModelIncognito)
String.format(stringResource(MR.strings.group_preview_join_as), currentUserProfileDisplayName ?: "") String.format(stringResource(R.string.group_preview_join_as), currentUserProfileDisplayName ?: "")
else else
stringResource(MR.strings.group_preview_you_are_invited) stringResource(R.string.group_preview_you_are_invited)
} }
@Composable @Composable
fun unreadCountStr(n: Int): String { fun unreadCountStr(n: Int): String {
return if (n < 1000) "$n" else "${n / 1000}" + stringResource(MR.strings.thousand_abbreviation) return if (n < 1000) "$n" else "${n / 1000}" + stringResource(R.string.thousand_abbreviation)
} }
@Composable @Composable
@@ -295,7 +278,7 @@ fun ChatStatusImage(s: NetworkStatus?) {
val descr = s?.statusString val descr = s?.statusString
if (s is NetworkStatus.Error) { if (s is NetworkStatus.Error) {
Icon( Icon(
painterResource(MR.images.ic_error), painterResource(R.drawable.ic_error),
contentDescription = descr, contentDescription = descr,
tint = MaterialTheme.colors.secondary, tint = MaterialTheme.colors.secondary,
modifier = Modifier modifier = Modifier
@@ -7,21 +7,21 @@ 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 dev.icerock.moko.resources.compose.painterResource 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
import chat.simplex.res.MR
@Composable @Composable
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) MR.images.ic_add_link else MR.images.ic_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
@@ -6,7 +6,7 @@ import androidx.compose.material.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import dev.icerock.moko.resources.compose.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.unit.dp import androidx.compose.ui.unit.dp
@@ -15,7 +15,6 @@ 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.ChatInfoImage import chat.simplex.app.views.helpers.ChatInfoImage
import chat.simplex.res.MR
@Composable @Composable
fun ContactRequestView(chatModelIncognito: Boolean, contactRequest: ChatInfo.ContactRequest) { fun ContactRequestView(chatModelIncognito: Boolean, contactRequest: ChatInfo.ContactRequest) {
@@ -35,7 +34,7 @@ fun ContactRequestView(chatModelIncognito: Boolean, contactRequest: ChatInfo.Con
color = if (chatModelIncognito) Indigo else MaterialTheme.colors.primary color = if (chatModelIncognito) Indigo else MaterialTheme.colors.primary
) )
val height = with(LocalDensity.current) { 46.sp.toDp() } val height = with(LocalDensity.current) { 46.sp.toDp() }
Text(stringResource(MR.strings.contact_wants_to_connect_with_you), Modifier.heightIn(min = height), maxLines = 2, color = if (isInDarkTheme()) MessagePreviewDark else MessagePreviewLight) Text(stringResource(R.string.contact_wants_to_connect_with_you), Modifier.heightIn(min = height), maxLines = 2, color = if (isInDarkTheme()) MessagePreviewDark else MessagePreviewLight)
} }
val ts = getTimestampText(contactRequest.contactRequest.updatedAt) val ts = getTimestampText(contactRequest.contactRequest.updatedAt)
Column( Column(
@@ -11,8 +11,8 @@ 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 dev.icerock.moko.resources.compose.painterResource import androidx.compose.ui.res.painterResource
import dev.icerock.moko.resources.compose.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
@@ -21,7 +21,6 @@ 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.*
import chat.simplex.res.MR
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
@Composable @Composable
@@ -36,6 +35,7 @@ fun ShareListView(chatModel: ChatModel, stopped: Boolean) {
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.themedBackground()
) { ) {
if (chatModel.chats.isNotEmpty()) { if (chatModel.chats.isNotEmpty()) {
ShareList(chatModel, search = searchInList) ShareList(chatModel, search = searchInList)
@@ -53,7 +53,7 @@ fun ShareListView(chatModel: ChatModel, stopped: Boolean) {
@Composable @Composable
private fun EmptyList() { private fun EmptyList() {
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Text(stringResource(MR.strings.you_have_no_chats), color = MaterialTheme.colors.secondary) Text(stringResource(R.string.you_have_no_chats), color = MaterialTheme.colors.secondary)
} }
} }
@@ -83,7 +83,7 @@ private fun ShareListToolbar(chatModel: ChatModel, userPickerState: MutableState
if (chatModel.chats.size >= 8) { if (chatModel.chats.size >= 8) {
barButtons.add { barButtons.add {
IconButton({ showSearch = true }) { IconButton({ showSearch = true }) {
Icon(painterResource(MR.images.ic_search_500), stringResource(MR.strings.search_verb).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)
} }
} }
} }
@@ -91,13 +91,13 @@ private fun ShareListToolbar(chatModel: ChatModel, userPickerState: MutableState
barButtons.add { barButtons.add {
IconButton(onClick = { IconButton(onClick = {
AlertManager.shared.showAlertMsg( AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.chat_is_stopped_indication), generalGetString(R.string.chat_is_stopped_indication),
generalGetString(MR.strings.you_can_start_chat_via_setting_or_by_restarting_the_app) generalGetString(R.string.you_can_start_chat_via_setting_or_by_restarting_the_app)
) )
}) { }) {
Icon( Icon(
painterResource(MR.images.ic_report_filled), painterResource(R.drawable.ic_report_filled),
generalGetString(MR.strings.chat_is_stopped_indication), generalGetString(R.string.chat_is_stopped_indication),
tint = Color.Red, tint = Color.Red,
) )
} }
@@ -110,18 +110,18 @@ private fun ShareListToolbar(chatModel: ChatModel, userPickerState: MutableState
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
Text( Text(
when (chatModel.sharedContent.value) { when (chatModel.sharedContent.value) {
is SharedContent.Text -> stringResource(MR.strings.share_message) is SharedContent.Text -> stringResource(R.string.share_message)
is SharedContent.Media -> stringResource(MR.strings.share_image) is SharedContent.Media -> stringResource(R.string.share_image)
is SharedContent.File -> stringResource(MR.strings.share_file) is SharedContent.File -> stringResource(R.string.share_file)
else -> stringResource(MR.strings.share_message) else -> stringResource(R.string.share_message)
}, },
color = MaterialTheme.colors.onBackground, color = MaterialTheme.colors.onBackground,
fontWeight = FontWeight.SemiBold, fontWeight = FontWeight.SemiBold,
) )
if (chatModel.incognito.value) { if (chatModel.incognito.value) {
Icon( Icon(
painterResource(MR.images.ic_theater_comedy_filled), painterResource(R.drawable.ic_theater_comedy_filled),
stringResource(MR.strings.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)
) )
@@ -15,7 +15,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.graphicsLayer
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 dev.icerock.moko.resources.compose.painterResource import androidx.compose.ui.res.painterResource
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
@@ -26,7 +26,6 @@ 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.helpers.* import chat.simplex.app.views.helpers.*
import chat.simplex.res.MR
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -168,9 +167,9 @@ fun UserProfilePickerItem(u: User, unreadCount: Int = 0, padding: PaddingValues
) { ) {
UserProfileRow(u) UserProfileRow(u)
if (u.activeUser) { if (u.activeUser) {
Icon(painterResource(MR.images.ic_done_filled), null, Modifier.size(20.dp), tint = MaterialTheme.colors.onBackground) Icon(painterResource(R.drawable.ic_done_filled), null, Modifier.size(20.dp), tint = MaterialTheme.colors.onBackground)
} else if (u.hidden) { } else if (u.hidden) {
Icon(painterResource(MR.images.ic_lock), null, Modifier.size(20.dp), tint = MaterialTheme.colors.secondary) Icon(painterResource(R.drawable.ic_lock), null, Modifier.size(20.dp), tint = MaterialTheme.colors.secondary)
} else if (unreadCount > 0) { } else if (unreadCount > 0) {
Box( Box(
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
@@ -186,7 +185,7 @@ fun UserProfilePickerItem(u: User, unreadCount: Int = 0, padding: PaddingValues
) )
} }
} else if (!u.showNtfs) { } else if (!u.showNtfs) {
Icon(painterResource(MR.images.ic_notifications_off), null, Modifier.size(20.dp), tint = MaterialTheme.colors.secondary) Icon(painterResource(R.drawable.ic_notifications_off), null, Modifier.size(20.dp), tint = MaterialTheme.colors.secondary)
} else { } else {
Box(Modifier.size(20.dp)) Box(Modifier.size(20.dp))
} }
@@ -218,8 +217,8 @@ fun UserProfileRow(u: User) {
@Composable @Composable
private fun SettingsPickerItem(onClick: () -> Unit) { private fun SettingsPickerItem(onClick: () -> Unit) {
SectionItemView(onClick, padding = PaddingValues(start = DEFAULT_PADDING + 7.dp, end = DEFAULT_PADDING), minHeight = 68.dp) { SectionItemView(onClick, padding = PaddingValues(start = DEFAULT_PADDING + 7.dp, end = DEFAULT_PADDING), minHeight = 68.dp) {
val text = generalGetString(MR.strings.settings_section_title_settings).lowercase().capitalize(Locale.current) val text = generalGetString(R.string.settings_section_title_settings).lowercase().capitalize(Locale.current)
Icon(painterResource(MR.images.ic_settings), text, Modifier.size(20.dp), tint = MaterialTheme.colors.onBackground) Icon(painterResource(R.drawable.ic_settings), text, Modifier.size(20.dp), tint = MaterialTheme.colors.onBackground)
Spacer(Modifier.width(DEFAULT_PADDING + 6.dp)) Spacer(Modifier.width(DEFAULT_PADDING + 6.dp))
Text( Text(
text, text,
@@ -231,8 +230,8 @@ private fun SettingsPickerItem(onClick: () -> Unit) {
@Composable @Composable
private fun CancelPickerItem(onClick: () -> Unit) { private fun CancelPickerItem(onClick: () -> Unit) {
SectionItemView(onClick, padding = PaddingValues(start = DEFAULT_PADDING + 7.dp, end = DEFAULT_PADDING), minHeight = 68.dp) { SectionItemView(onClick, padding = PaddingValues(start = DEFAULT_PADDING + 7.dp, end = DEFAULT_PADDING), minHeight = 68.dp) {
val text = generalGetString(MR.strings.cancel_verb) val text = generalGetString(R.string.cancel_verb)
Icon(painterResource(MR.images.ic_close), text, Modifier.size(20.dp), tint = MaterialTheme.colors.onBackground) Icon(painterResource(R.drawable.ic_close), text, Modifier.size(20.dp), tint = MaterialTheme.colors.onBackground)
Spacer(Modifier.width(DEFAULT_PADDING + 6.dp)) Spacer(Modifier.width(DEFAULT_PADDING + 6.dp))
Text( Text(
text, text,
@@ -14,19 +14,19 @@ 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.runtime.* import androidx.compose.runtime.*
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 dev.icerock.moko.resources.compose.painterResource import androidx.compose.ui.res.painterResource
import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import chat.simplex.app.*
import chat.simplex.app.R import chat.simplex.app.R
import chat.simplex.app.TAG
import chat.simplex.app.model.ChatModel import chat.simplex.app.model.ChatModel
import chat.simplex.app.ui.theme.SimpleXTheme import chat.simplex.app.ui.theme.SimpleXTheme
import chat.simplex.app.views.helpers.* import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.usersettings.* import chat.simplex.app.views.usersettings.*
import chat.simplex.res.MR
import kotlinx.datetime.* import kotlinx.datetime.*
import java.io.BufferedOutputStream import java.io.BufferedOutputStream
import java.io.File import java.io.File
@@ -36,8 +36,8 @@ import java.util.*
@Composable @Composable
fun ChatArchiveView(m: ChatModel, title: String, archiveName: String, archiveTime: Instant) { fun ChatArchiveView(m: ChatModel, title: String, archiveName: String, archiveTime: Instant) {
val context = LocalContext.current val context = LocalContext.current
val archivePath = "${getFilesDirectory()}/$archiveName" val archivePath = "${getFilesDirectory(context)}/$archiveName"
val saveArchiveLauncher = rememberSaveArchiveLauncher(archivePath) val saveArchiveLauncher = rememberSaveArchiveLauncher(cxt = context, archivePath)
ChatArchiveLayout( ChatArchiveLayout(
title, title,
archiveTime, archiveTime,
@@ -57,17 +57,17 @@ fun ChatArchiveLayout(
Modifier.fillMaxWidth(), Modifier.fillMaxWidth(),
) { ) {
AppBarTitle(title) AppBarTitle(title)
SectionView(stringResource(MR.strings.chat_archive_section)) { SectionView(stringResource(R.string.chat_archive_section)) {
SettingsActionItem( SettingsActionItem(
painterResource(MR.images.ic_ios_share), painterResource(R.drawable.ic_ios_share),
stringResource(MR.strings.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,
) )
SettingsActionItem( SettingsActionItem(
painterResource(MR.images.ic_delete), painterResource(R.drawable.ic_delete),
stringResource(MR.strings.delete_archive), stringResource(R.string.delete_archive),
deleteArchiveAlert, deleteArchiveAlert,
textColor = Color.Red, textColor = Color.Red,
iconColor = Color.Red, iconColor = Color.Red,
@@ -75,18 +75,17 @@ fun ChatArchiveLayout(
} }
val archiveTs = SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z", Locale.US).format(Date.from(archiveTime.toJavaInstant())) val archiveTs = SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z", Locale.US).format(Date.from(archiveTime.toJavaInstant()))
SectionTextFooter( SectionTextFooter(
String.format(generalGetString(MR.strings.archive_created_on_ts), archiveTs) String.format(generalGetString(R.string.archive_created_on_ts), archiveTs)
) )
SectionBottomSpacer() SectionBottomSpacer()
} }
} }
@Composable @Composable
private fun rememberSaveArchiveLauncher(chatArchivePath: String): ManagedActivityResultLauncher<String, Uri?> = private fun rememberSaveArchiveLauncher(cxt: Context, chatArchivePath: String): ManagedActivityResultLauncher<String, Uri?> =
rememberLauncherForActivityResult( rememberLauncherForActivityResult(
contract = ActivityResultContracts.CreateDocument(), contract = ActivityResultContracts.CreateDocument(),
onResult = { destination -> onResult = { destination ->
val cxt = SimplexApp.context
try { try {
destination?.let { destination?.let {
val contentResolver = cxt.contentResolver val contentResolver = cxt.contentResolver
@@ -94,11 +93,11 @@ private fun rememberSaveArchiveLauncher(chatArchivePath: String): ManagedActivit
val outputStream = BufferedOutputStream(stream) val outputStream = BufferedOutputStream(stream)
File(chatArchivePath).inputStream().use { it.copyTo(outputStream) } File(chatArchivePath).inputStream().use { it.copyTo(outputStream) }
outputStream.close() outputStream.close()
Toast.makeText(cxt, generalGetString(MR.strings.file_saved), Toast.LENGTH_SHORT).show() Toast.makeText(cxt, generalGetString(R.string.file_saved), Toast.LENGTH_SHORT).show()
} }
} }
} catch (e: Error) { } catch (e: Error) {
Toast.makeText(cxt, generalGetString(MR.strings.error_saving_file), Toast.LENGTH_SHORT).show() Toast.makeText(cxt, generalGetString(R.string.error_saving_file), Toast.LENGTH_SHORT).show()
Log.e(TAG, "rememberSaveArchiveLauncher error saving archive $e") Log.e(TAG, "rememberSaveArchiveLauncher error saving archive $e")
} }
} }
@@ -106,8 +105,8 @@ private fun rememberSaveArchiveLauncher(chatArchivePath: String): ManagedActivit
private fun deleteArchiveAlert(m: ChatModel, archivePath: String) { private fun deleteArchiveAlert(m: ChatModel, archivePath: String) {
AlertManager.shared.showAlertDialog( AlertManager.shared.showAlertDialog(
title = generalGetString(MR.strings.delete_chat_archive_question), title = generalGetString(R.string.delete_chat_archive_question),
confirmText = generalGetString(MR.strings.delete_verb), confirmText = generalGetString(R.string.delete_verb),
onConfirm = { onConfirm = {
val fileDeleted = File(archivePath).delete() val fileDeleted = File(archivePath).delete()
if (fileDeleted) { if (fileDeleted) {
@@ -18,8 +18,8 @@ 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 dev.icerock.moko.resources.compose.painterResource import androidx.compose.ui.res.painterResource
import dev.icerock.moko.resources.compose.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.*
@@ -30,7 +30,6 @@ 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 chat.simplex.res.MR
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.datetime.Clock import kotlinx.datetime.Clock
import kotlin.math.log2 import kotlin.math.log2
@@ -72,14 +71,14 @@ fun DatabaseEncryptionView(m: ChatModel) {
sqliteError is SQLiteError.ErrorNotADatabase -> { sqliteError is SQLiteError.ErrorNotADatabase -> {
operationEnded(m, progressIndicator) { operationEnded(m, progressIndicator) {
AlertManager.shared.showAlertMsg( AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.wrong_passphrase_title), generalGetString(R.string.wrong_passphrase_title),
generalGetString(MR.strings.enter_correct_current_passphrase) generalGetString(R.string.enter_correct_current_passphrase)
) )
} }
} }
error != null -> { error != null -> {
operationEnded(m, progressIndicator) { operationEnded(m, progressIndicator) {
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_encrypting_database), AlertManager.shared.showAlertMsg(generalGetString(R.string.error_encrypting_database),
"failed to set storage encryption: ${error.responseType} ${error.details}" "failed to set storage encryption: ${error.responseType} ${error.details}"
) )
} }
@@ -92,13 +91,13 @@ fun DatabaseEncryptionView(m: ChatModel) {
} }
resetFormAfterEncryption(m, initialRandomDBPassphrase, currentKey, newKey, confirmNewKey, storedKey, useKeychain.value) resetFormAfterEncryption(m, initialRandomDBPassphrase, currentKey, newKey, confirmNewKey, storedKey, useKeychain.value)
operationEnded(m, progressIndicator) { operationEnded(m, progressIndicator) {
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.database_encrypted)) AlertManager.shared.showAlertMsg(generalGetString(R.string.database_encrypted))
} }
} }
} }
} catch (e: Exception) { } catch (e: Exception) {
operationEnded(m, progressIndicator) { operationEnded(m, progressIndicator) {
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_encrypting_database), e.stackTraceToString()) AlertManager.shared.showAlertMsg(generalGetString(R.string.error_encrypting_database), e.stackTraceToString())
} }
} }
} }
@@ -137,16 +136,16 @@ fun DatabaseEncryptionLayout(
Column( Column(
Modifier.fillMaxWidth().verticalScroll(rememberScrollState()), Modifier.fillMaxWidth().verticalScroll(rememberScrollState()),
) { ) {
AppBarTitle(stringResource(MR.strings.database_passphrase)) AppBarTitle(stringResource(R.string.database_passphrase))
SectionView(null) { SectionView(null) {
SavePassphraseSetting(useKeychain.value, initialRandomDBPassphrase.value, storedKey.value, progressIndicator.value) { checked -> SavePassphraseSetting(useKeychain.value, initialRandomDBPassphrase.value, storedKey.value, progressIndicator.value) { checked ->
if (checked) { if (checked) {
setUseKeychain(true, useKeychain, prefs) setUseKeychain(true, useKeychain, prefs)
} else if (storedKey.value) { } else if (storedKey.value) {
AlertManager.shared.showAlertDialog( AlertManager.shared.showAlertDialog(
title = generalGetString(MR.strings.remove_passphrase_from_keychain), title = generalGetString(R.string.remove_passphrase_from_keychain),
text = generalGetString(MR.strings.notifications_will_be_hidden) + "\n" + storeSecurelyDanger(), text = generalGetString(R.string.notifications_will_be_hidden) + "\n" + storeSecurelyDanger(),
confirmText = generalGetString(MR.strings.remove_passphrase), confirmText = generalGetString(R.string.remove_passphrase),
onConfirm = { onConfirm = {
DatabaseUtils.ksDatabasePassword.remove() DatabaseUtils.ksDatabasePassword.remove()
setUseKeychain(false, useKeychain, prefs) setUseKeychain(false, useKeychain, prefs)
@@ -162,7 +161,7 @@ fun DatabaseEncryptionLayout(
if (!initialRandomDBPassphrase.value && chatDbEncrypted == true) { if (!initialRandomDBPassphrase.value && chatDbEncrypted == true) {
PassphraseField( PassphraseField(
currentKey, currentKey,
generalGetString(MR.strings.current_passphrase), generalGetString(R.string.current_passphrase),
modifier = Modifier.padding(horizontal = DEFAULT_PADDING), modifier = Modifier.padding(horizontal = DEFAULT_PADDING),
isValid = ::validKey, isValid = ::validKey,
keyboardActions = KeyboardActions(onNext = { defaultKeyboardAction(ImeAction.Next) }), keyboardActions = KeyboardActions(onNext = { defaultKeyboardAction(ImeAction.Next) }),
@@ -171,7 +170,7 @@ fun DatabaseEncryptionLayout(
PassphraseField( PassphraseField(
newKey, newKey,
generalGetString(MR.strings.new_passphrase), generalGetString(R.string.new_passphrase),
modifier = Modifier.padding(horizontal = DEFAULT_PADDING), modifier = Modifier.padding(horizontal = DEFAULT_PADDING),
showStrength = true, showStrength = true,
isValid = ::validKey, isValid = ::validKey,
@@ -202,7 +201,7 @@ fun DatabaseEncryptionLayout(
PassphraseField( PassphraseField(
confirmNewKey, confirmNewKey,
generalGetString(MR.strings.confirm_new_passphrase), generalGetString(R.string.confirm_new_passphrase),
modifier = Modifier.padding(horizontal = DEFAULT_PADDING), modifier = Modifier.padding(horizontal = DEFAULT_PADDING),
isValid = { confirmNewKey.value == "" || newKey.value == confirmNewKey.value }, isValid = { confirmNewKey.value == "" || newKey.value == confirmNewKey.value },
keyboardActions = KeyboardActions(onDone = { keyboardActions = KeyboardActions(onDone = {
@@ -212,27 +211,27 @@ fun DatabaseEncryptionLayout(
) )
SectionItemViewSpaceBetween(onClickUpdate, disabled = disabled, minHeight = TextFieldDefaults.MinHeight) { SectionItemViewSpaceBetween(onClickUpdate, disabled = disabled, minHeight = TextFieldDefaults.MinHeight) {
Text(generalGetString(MR.strings.update_database_passphrase), color = if (disabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary) Text(generalGetString(R.string.update_database_passphrase), color = if (disabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary)
} }
} }
Column { Column {
if (chatDbEncrypted == false) { if (chatDbEncrypted == false) {
SectionTextFooter(generalGetString(MR.strings.database_is_not_encrypted)) SectionTextFooter(generalGetString(R.string.database_is_not_encrypted))
} else if (useKeychain.value) { } else if (useKeychain.value) {
if (storedKey.value) { if (storedKey.value) {
SectionTextFooter(generalGetString(MR.strings.keychain_is_storing_securely)) SectionTextFooter(generalGetString(R.string.keychain_is_storing_securely))
if (initialRandomDBPassphrase.value) { if (initialRandomDBPassphrase.value) {
SectionTextFooter(generalGetString(MR.strings.encrypted_with_random_passphrase)) SectionTextFooter(generalGetString(R.string.encrypted_with_random_passphrase))
} else { } else {
SectionTextFooter(generalGetString(MR.strings.impossible_to_recover_passphrase)) SectionTextFooter(generalGetString(R.string.impossible_to_recover_passphrase))
} }
} else { } else {
SectionTextFooter(generalGetString(MR.strings.keychain_allows_to_receive_ntfs)) SectionTextFooter(generalGetString(R.string.keychain_allows_to_receive_ntfs))
} }
} else { } else {
SectionTextFooter(generalGetString(MR.strings.you_have_to_enter_passphrase_every_time)) SectionTextFooter(generalGetString(R.string.you_have_to_enter_passphrase_every_time))
SectionTextFooter(generalGetString(MR.strings.impossible_to_recover_passphrase)) SectionTextFooter(generalGetString(R.string.impossible_to_recover_passphrase))
} }
} }
SectionBottomSpacer() SectionBottomSpacer()
@@ -241,9 +240,9 @@ fun DatabaseEncryptionLayout(
fun encryptDatabaseSavedAlert(onConfirm: () -> Unit) { fun encryptDatabaseSavedAlert(onConfirm: () -> Unit) {
AlertManager.shared.showAlertDialog( AlertManager.shared.showAlertDialog(
title = generalGetString(MR.strings.encrypt_database_question), title = generalGetString(R.string.encrypt_database_question),
text = generalGetString(MR.strings.database_will_be_encrypted_and_passphrase_stored) + "\n" + storeSecurelySaved(), text = generalGetString(R.string.database_will_be_encrypted_and_passphrase_stored) + "\n" + storeSecurelySaved(),
confirmText = generalGetString(MR.strings.encrypt_database), confirmText = generalGetString(R.string.encrypt_database),
onConfirm = onConfirm, onConfirm = onConfirm,
destructive = true, destructive = true,
) )
@@ -251,9 +250,9 @@ fun encryptDatabaseSavedAlert(onConfirm: () -> Unit) {
fun encryptDatabaseAlert(onConfirm: () -> Unit) { fun encryptDatabaseAlert(onConfirm: () -> Unit) {
AlertManager.shared.showAlertDialog( AlertManager.shared.showAlertDialog(
title = generalGetString(MR.strings.encrypt_database_question), title = generalGetString(R.string.encrypt_database_question),
text = generalGetString(MR.strings.database_will_be_encrypted) +"\n" + storeSecurelyDanger(), text = generalGetString(R.string.database_will_be_encrypted) +"\n" + storeSecurelyDanger(),
confirmText = generalGetString(MR.strings.encrypt_database), confirmText = generalGetString(R.string.encrypt_database),
onConfirm = onConfirm, onConfirm = onConfirm,
destructive = true, destructive = true,
) )
@@ -261,9 +260,9 @@ fun encryptDatabaseAlert(onConfirm: () -> Unit) {
fun changeDatabaseKeySavedAlert(onConfirm: () -> Unit) { fun changeDatabaseKeySavedAlert(onConfirm: () -> Unit) {
AlertManager.shared.showAlertDialog( AlertManager.shared.showAlertDialog(
title = generalGetString(MR.strings.change_database_passphrase_question), title = generalGetString(R.string.change_database_passphrase_question),
text = generalGetString(MR.strings.database_encryption_will_be_updated) + "\n" + storeSecurelySaved(), text = generalGetString(R.string.database_encryption_will_be_updated) + "\n" + storeSecurelySaved(),
confirmText = generalGetString(MR.strings.update_database), confirmText = generalGetString(R.string.update_database),
onConfirm = onConfirm, onConfirm = onConfirm,
destructive = false, destructive = false,
) )
@@ -271,9 +270,9 @@ fun changeDatabaseKeySavedAlert(onConfirm: () -> Unit) {
fun changeDatabaseKeyAlert(onConfirm: () -> Unit) { fun changeDatabaseKeyAlert(onConfirm: () -> Unit) {
AlertManager.shared.showAlertDialog( AlertManager.shared.showAlertDialog(
title = generalGetString(MR.strings.change_database_passphrase_question), title = generalGetString(R.string.change_database_passphrase_question),
text = generalGetString(MR.strings.database_passphrase_will_be_updated) + "\n" + storeSecurelyDanger(), text = generalGetString(R.string.database_passphrase_will_be_updated) + "\n" + storeSecurelyDanger(),
confirmText = generalGetString(MR.strings.update_database), confirmText = generalGetString(R.string.update_database),
onConfirm = onConfirm, onConfirm = onConfirm,
destructive = true, destructive = true,
) )
@@ -291,13 +290,13 @@ fun SavePassphraseSetting(
SectionItemView(minHeight = minHeight) { SectionItemView(minHeight = minHeight) {
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
Icon( Icon(
if (storedKey) painterResource(MR.images.ic_vpn_key_filled) else painterResource(MR.images.ic_vpn_key_off_filled), if (storedKey) painterResource(R.drawable.ic_vpn_key_filled) else painterResource(R.drawable.ic_vpn_key_off_filled),
stringResource(MR.strings.save_passphrase_in_keychain), stringResource(R.string.save_passphrase_in_keychain),
tint = if (storedKey) SimplexGreen else MaterialTheme.colors.secondary tint = if (storedKey) SimplexGreen else MaterialTheme.colors.secondary
) )
Spacer(Modifier.padding(horizontal = 4.dp)) Spacer(Modifier.padding(horizontal = 4.dp))
Text( Text(
stringResource(MR.strings.save_passphrase_in_keychain), stringResource(R.string.save_passphrase_in_keychain),
Modifier.padding(end = 24.dp), Modifier.padding(end = 24.dp),
color = Color.Unspecified color = Color.Unspecified
) )
@@ -334,9 +333,9 @@ fun setUseKeychain(value: Boolean, useKeychain: MutableState<Boolean>, prefs: Ap
prefs.storeDBPassphrase.set(value) prefs.storeDBPassphrase.set(value)
} }
fun storeSecurelySaved() = generalGetString(MR.strings.store_passphrase_securely) fun storeSecurelySaved() = generalGetString(R.string.store_passphrase_securely)
fun storeSecurelyDanger() = generalGetString(MR.strings.store_passphrase_securely_without_recover) fun storeSecurelyDanger() = generalGetString(R.string.store_passphrase_securely_without_recover)
private fun operationEnded(m: ChatModel, progressIndicator: MutableState<Boolean>, alert: () -> Unit) { private fun operationEnded(m: ChatModel, progressIndicator: MutableState<Boolean>, alert: () -> Unit) {
m.chatDbChanged.value = true m.chatDbChanged.value = true
@@ -358,8 +357,8 @@ fun PassphraseField(
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) painterResource(MR.images.ic_visibility_off_filled) else painterResource(MR.images.ic_visibility_filled) if (showKey) painterResource(R.drawable.ic_visibility_off_filled) else painterResource(R.drawable.ic_visibility_filled)
} else painterResource(MR.images.ic_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 MaterialTheme.colors.secondary if (showStrength && key.value.isNotEmpty()) PassphraseStrength.check(key.value).color else MaterialTheme.colors.secondary
} else Color.Red } else Color.Red
@@ -498,7 +497,7 @@ fun PreviewDatabaseEncryptionLayout() {
SimpleXTheme { SimpleXTheme {
DatabaseEncryptionLayout( DatabaseEncryptionLayout(
useKeychain = remember { mutableStateOf(true) }, useKeychain = remember { mutableStateOf(true) },
prefs = AppPreferences(), prefs = AppPreferences(SimplexApp.context),
chatDbEncrypted = true, chatDbEncrypted = true,
currentKey = remember { mutableStateOf("") }, currentKey = remember { mutableStateOf("") },
newKey = remember { mutableStateOf("") }, newKey = remember { mutableStateOf("") },

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