mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2024-12-17 17:20:21 +01:00
Compare commits
44 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bcce6a18a1 | |||
| 3bce760e05 | |||
| a7c60f0721 | |||
| 94ad68155f | |||
| cb2b5ff97b | |||
| 313c40590f | |||
| d806efc347 | |||
| 743db49215 | |||
| 4fdffdb8aa | |||
| 1699aae906 | |||
| 82eba49b95 | |||
| 218b1a8b7e | |||
| 6c1bba55b4 | |||
| c9fc1547b0 | |||
| 8a3a8455d4 | |||
| 9c437f6204 | |||
| ef698e58d4 | |||
| 01c0ac3d13 | |||
| ed7089902c | |||
| 1ddd4193a0 | |||
| 932875fd9d | |||
| 1899c84ee2 | |||
| 91e03a016f | |||
| a978c12ca3 | |||
| da1f2d9ed6 | |||
| 1ed6179782 | |||
| e15251e354 | |||
| c348dd765f | |||
| 78a23e6b02 | |||
| 590499684d | |||
| 59c50f8088 | |||
| ee4c759706 | |||
| d5bd3a7d68 | |||
| 7159195cb9 | |||
| 45d3855d84 | |||
| 650a6f0758 | |||
| ae6ba0cfb5 | |||
| d9a2317f82 | |||
| 3d8b521c0c | |||
| 1fd5bbbc4f | |||
| 62299cbf0b | |||
| 3eb969d3c5 | |||
| 85dba0f36b | |||
| cb6490ed59 |
@@ -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,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,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
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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)|[](https://hosted.weblate.org/projects/simplex-chat/android/es/)<br>[](https://hosted.weblate.org/projects/simplex-chat/ios/es/)|[](https://hosted.weblate.org/projects/simplex-chat/website/es/)||
|
|🇪🇸 es|Español |[Mateyhv](https://github.com/Mateyhv)|[](https://hosted.weblate.org/projects/simplex-chat/android/es/)<br>[](https://hosted.weblate.org/projects/simplex-chat/ios/es/)|[](https://hosted.weblate.org/projects/simplex-chat/website/es/)||
|
||||||
|🇫🇷 fr|Français |[ishi_sama](https://github.com/ishi-sama)|[](https://hosted.weblate.org/projects/simplex-chat/android/fr/)<br>[](https://hosted.weblate.org/projects/simplex-chat/ios/fr/)|[](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)|[](https://hosted.weblate.org/projects/simplex-chat/android/fr/)<br>[](https://hosted.weblate.org/projects/simplex-chat/ios/fr/)|[](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)|[](https://hosted.weblate.org/projects/simplex-chat/android/it/)<br>[](https://hosted.weblate.org/projects/simplex-chat/ios/it/)|[](https://hosted.weblate.org/projects/simplex-chat/website/it/)||
|
|🇮🇹 it|Italiano |[unbranched](https://github.com/unbranched)|[](https://hosted.weblate.org/projects/simplex-chat/android/it/)<br>[](https://hosted.weblate.org/projects/simplex-chat/ios/it/)|[](https://hosted.weblate.org/projects/simplex-chat/website/it/)||
|
||||||
|🇯🇵 ja|Japanese ||[](https://hosted.weblate.org/projects/simplex-chat/android/ja/)<br>[](https://hosted.weblate.org/projects/simplex-chat/ios/ja/)|||
|
|
||||||
|🇳🇱 nl|Nederlands|[mika-nl](https://github.com/mika-nl)|[](https://hosted.weblate.org/projects/simplex-chat/android/nl/)<br>[](https://hosted.weblate.org/projects/simplex-chat/ios/nl/)|[](https://hosted.weblate.org/projects/simplex-chat/website/nl/)||
|
|🇳🇱 nl|Nederlands|[mika-nl](https://github.com/mika-nl)|[](https://hosted.weblate.org/projects/simplex-chat/android/nl/)<br>[](https://hosted.weblate.org/projects/simplex-chat/ios/nl/)|[](https://hosted.weblate.org/projects/simplex-chat/website/nl/)||
|
||||||
|🇵🇱 pl|Polski |[BxOxSxS](https://github.com/BxOxSxS)|[](https://hosted.weblate.org/projects/simplex-chat/android/ru/)<br>[](https://hosted.weblate.org/projects/simplex-chat/ios/pl/)|||
|
|🇵🇱 pl|Polski |[BxOxSxS](https://github.com/BxOxSxS)|[](https://hosted.weblate.org/projects/simplex-chat/android/ru/)<br>[](https://hosted.weblate.org/projects/simplex-chat/ios/pl/)|||
|
||||||
|🇧🇷 pt-BR|Português||[](https://hosted.weblate.org/projects/simplex-chat/android/pt_BR/)<br>-|[](https://hosted.weblate.org/projects/simplex-chat/website/pt_BR/)||
|
|
||||||
|🇷🇺 ru|Русский ||[](https://hosted.weblate.org/projects/simplex-chat/android/ru/)<br>[](https://hosted.weblate.org/projects/simplex-chat/ios/ru/)|||
|
|🇷🇺 ru|Русский ||[](https://hosted.weblate.org/projects/simplex-chat/android/ru/)<br>[](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)|[](https://hosted.weblate.org/projects/simplex-chat/android/zh_Hans/)<br>[](https://hosted.weblate.org/projects/simplex-chat/ios/zh_Hans/)<br> |<br><br>[](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)|[](https://hosted.weblate.org/projects/simplex-chat/android/zh_Hans/)<br>[](https://hosted.weblate.org/projects/simplex-chat/ios/zh_Hans/)<br> |<br><br>[](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.
|
||||||
|
|
||||||
|
|||||||
@@ -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/
|
||||||
Generated
+1
@@ -0,0 +1 @@
|
|||||||
|
SimpleX
|
||||||
Generated
Generated
+6
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="CompilerConfiguration">
|
||||||
|
<bytecodeTargetLevel target="11" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
Generated
+19
@@ -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>
|
||||||
Generated
+6
@@ -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).
|
|
||||||
@@ -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" }
|
||||||
|
}
|
||||||
|
}
|
||||||
-1
@@ -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 |
+87
-143
@@ -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")
|
||||||
+32
-44
@@ -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))
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
+15
-193
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+145
-397
File diff suppressed because it is too large
Load Diff
+44
-56
@@ -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")
|
||||||
}
|
}
|
||||||
+425
-673
File diff suppressed because it is too large
Load Diff
+1
@@ -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)
|
||||||
+12
-28
@@ -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(
|
||||||
+22
-20
@@ -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()
|
||||||
+8
-13
@@ -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,
|
||||||
+1
-1
@@ -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)
|
||||||
+4
-4
@@ -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))
|
||||||
}
|
}
|
||||||
+72
-73
@@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+1
@@ -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)
|
||||||
+14
-17
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
+15
-18
@@ -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
-12
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
+2
-2
@@ -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) {
|
||||||
+21
-20
@@ -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 = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
+44
-148
@@ -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 = { _, _ -> },
|
||||||
+6
-7
@@ -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)
|
||||||
)
|
)
|
||||||
+5
-6
@@ -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,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
+50
-97
@@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
+27
-28
@@ -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,
|
||||||
)
|
)
|
||||||
+6
-7
@@ -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)
|
||||||
) {}
|
) {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+4
-5
@@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+71
-216
@@ -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
|
||||||
+15
-14
@@ -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)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+30
-62
@@ -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 = {},
|
||||||
+38
-66
@@ -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() {
|
||||||
+16
-15
@@ -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,
|
||||||
+349
@@ -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 = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
+15
-34
@@ -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,
|
||||||
)
|
)
|
||||||
+10
-11
@@ -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,
|
||||||
)
|
)
|
||||||
+95
@@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
+10
-11
@@ -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 {
|
||||||
+1
-2
@@ -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 + " ") }
|
||||||
+22
-26
@@ -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
-9
@@ -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)) {
|
||||||
+22
-24
@@ -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) -> {} // ?
|
||||||
+8
-6
@@ -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())) {
|
||||||
+12
-15
@@ -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) {
|
||||||
+24
@@ -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)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
+22
-24
@@ -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)))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+35
-89
@@ -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 = { _, _, _ -> }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
+12
-12
@@ -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
|
||||||
+3
-4
@@ -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)
|
||||||
+9
-10
@@ -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)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+3
-5
@@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
+27
-18
@@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+63
-109
@@ -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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+27
-85
@@ -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())
|
|
||||||
|
|
||||||
+21
-38
@@ -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
|
||||||
+3
-3
@@ -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
|
||||||
+2
-3
@@ -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(
|
||||||
+15
-15
@@ -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)
|
||||||
)
|
)
|
||||||
+8
-9
@@ -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,
|
||||||
+17
-18
@@ -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) {
|
||||||
+42
-43
@@ -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
Reference in New Issue
Block a user