Compare commits
44 Commits
v5.2.0-bet
...
docs-polis
| 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 |
2
.github/ISSUE_TEMPLATE/bug.yml
vendored
2
.github/ISSUE_TEMPLATE/bug.yml
vendored
@@ -1,7 +1,7 @@
|
||||
name: Bug
|
||||
description: File a bug report/issue
|
||||
title: "[Bug]: "
|
||||
labels: ["bug", "triage"]
|
||||
labels: ["type:bug", "type:triage"]
|
||||
body:
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/feature.yml
vendored
2
.github/ISSUE_TEMPLATE/feature.yml
vendored
@@ -1,7 +1,7 @@
|
||||
name: Feature
|
||||
description: Suggest your feature
|
||||
title: "[Feature]: "
|
||||
labels: ["enhancement", "triage"]
|
||||
labels: ["type:enhancement", "type:triage"]
|
||||
body:
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/question.yml
vendored
2
.github/ISSUE_TEMPLATE/question.yml
vendored
@@ -1,7 +1,7 @@
|
||||
name: Question
|
||||
description: Ask your question
|
||||
title: "[Q]: "
|
||||
labels: ["question", "triage"]
|
||||
labels: ["type:question", "type:triage"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
|
||||
30
.github/workflows/build.yml
vendored
30
.github/workflows/build.yml
vendored
@@ -16,11 +16,11 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Clone project
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Build changelog
|
||||
id: build_changelog
|
||||
uses: mikepenz/release-changelog-builder-action@v4
|
||||
uses: mikepenz/release-changelog-builder-action@v1
|
||||
with:
|
||||
configuration: .github/changelog_conf.json
|
||||
failOnError: true
|
||||
@@ -62,25 +62,17 @@ jobs:
|
||||
cache_path: C:/cabal
|
||||
asset_name: simplex-chat-windows-x86-64
|
||||
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
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Setup Haskell
|
||||
uses: haskell/actions/setup@v2
|
||||
uses: haskell/actions/setup@v1
|
||||
with:
|
||||
ghc-version: "8.10.7"
|
||||
cabal-version: "latest"
|
||||
|
||||
- name: Cache dependencies
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: |
|
||||
${{ matrix.cache_path }}
|
||||
@@ -119,6 +111,12 @@ jobs:
|
||||
cabal build --enable-tests
|
||||
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
|
||||
if: startsWith(github.ref, 'refs/tags/v') && matrix.os != 'windows-latest'
|
||||
uses: svenstaro/upload-release-action@v2
|
||||
@@ -128,12 +126,6 @@ jobs:
|
||||
asset_name: ${{ matrix.asset_name }}
|
||||
tag: ${{ github.ref }}
|
||||
|
||||
- name: Unix test
|
||||
if: matrix.os != 'windows-latest'
|
||||
timeout-minutes: 30
|
||||
shell: bash
|
||||
run: cabal test --test-show-details=direct
|
||||
|
||||
# Unix /
|
||||
|
||||
# / Windows
|
||||
|
||||
2
.github/workflows/web.yml
vendored
2
.github/workflows/web.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [16.x]
|
||||
node-version: [12.x]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -49,8 +49,8 @@ logs/
|
||||
# for website
|
||||
website/node_modules/
|
||||
website/src/blog/
|
||||
website/src/docs/
|
||||
website/translations.json
|
||||
website/src/_data/supported_languages.json
|
||||
website/src/img/images/
|
||||
website/src/images/
|
||||
# Generated files
|
||||
|
||||
48
README.md
48
README.md
@@ -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.
|
||||
|
||||
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)
|
||||
|
||||
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.
|
||||
You can join an English-speaking users group if you want to ask any questions: [#SimpleX-Group-3](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2Fp-j-D_PrY2UMDchFHEUtbSES0nmzCnvD%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEA3gBfMjB_GDEmKQwjNdqGbnX91yfuZ7nRJgQijsx5Khc%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%2262MvNZ_Ec2mmlS8V0QNtLQ%3D%3D%22%7D)
|
||||
|
||||
There are groups in other languages, that we have the apps interface translated into. These groups are for testing, and asking questions to other SimpleX Chat users:
|
||||
|
||||
[\#SimpleX-DE](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FkIEl7OQzcp-J6aDmjdlQbRJwqkcZE7XR%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAR16PCu02MobRmKAsjzhDWMZcWP9hS8l5AUZi-Gs8z18%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22puYPMCQt11yPUvgmI5jCiw%3D%3D%22%7D) (German-speaking), [\#SimpleX-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.
|
||||
|
||||
@@ -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/)||
|
||||
|🇫🇷 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/)||
|
||||
|🇯🇵 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/)||
|
||||
|🇵🇱 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/)|||
|
||||
|🇨🇳 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:
|
||||
|
||||
- [share the color theme](./docs/THEMES.md) you use in Android app!
|
||||
- writing a tutorial or recipes about hosting servers, chat bot automations, etc.
|
||||
- contributing to SimpleX Chat knowledge-base.
|
||||
- developing features - please connect to us via chat so we can help you get started.
|
||||
@@ -207,8 +197,6 @@ You can use SimpleX with your own servers and still communicate with people usin
|
||||
|
||||
Recent updates:
|
||||
|
||||
[May 23, 2023. SimpleX Chat: v5.1 released with message reactions and self-destruct passcode](./blog/20230523-simplex-chat-v5-1-message-reactions-self-destruct-passcode.md).
|
||||
|
||||
[Apr 22, 2023. SimpleX Chat: vision and funding, v5.0 released with videos and files up to 1gb](./blog/20230422-simplex-chat-vision-funding-v5-videos-files-passcode.md).
|
||||
|
||||
[Mar 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).
|
||||
@@ -257,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.
|
||||
|
||||
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:
|
||||
|
||||
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.
|
||||
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.
|
||||
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.
|
||||
1. Instead of user profile identifiers used by all other platforms, even the most private ones, SimpleX uses pairwise per-queue identifiers (2 addresses for each unidirectional message queue, with an optional 3rd address for push notifications on iOS, 2 queues in each connection between the users). It makes observing the network graph on the application level more difficult, as for `n` users there can be up to `n * (n-1)` message queues.
|
||||
2. End-to-end encryption in each message queue using [NaCl cryptobox](https://nacl.cr.yp.to/box.html). This is added to allow redundancy in the future (passing each message via several servers), to avoid having the same ciphertext in different queues (that would only be visible to the attacker if TLS is compromised). The encryption keys used for this encryption are not rotated, instead we are planning to rotate the queues. Curve25519 keys are used for key negotiation.
|
||||
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).
|
||||
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.
|
||||
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.
|
||||
@@ -290,8 +276,6 @@ You can:
|
||||
|
||||
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
|
||||
|
||||
- ✅ Easy to deploy SimpleX server with in-memory message storage, without any dependencies.
|
||||
@@ -332,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).
|
||||
- ✅ Video messages.
|
||||
- ✅ App access passcode.
|
||||
- ✅ Improved Android app UI design.
|
||||
- ✅ Optional alternative access password.
|
||||
- ✅ Message reactions
|
||||
- ✅ Message editing history
|
||||
- ✅ Reduced battery and traffic usage in large groups.
|
||||
- 🏗 Desktop client.
|
||||
- 🏗 Message delivery confirmation (with sender opt-in or opt-out per contact, TBC).
|
||||
- SMP queue redundancy and rotation (manual is supported).
|
||||
- 🏗 Improved Android app UI design.
|
||||
- 🏗 SMP queue redundancy and rotation (manual is supported).
|
||||
- 🏗 Reduced battery and traffic usage in large groups.
|
||||
- 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.
|
||||
- Improved navigation and search in the conversation (expand and scroll to quoted message, scroll to search results, etc.).
|
||||
- Large groups, communities and public channels.
|
||||
- Feeds/broadcasts.
|
||||
- Ephemeral/disappearing/OTR conversations with the existing contacts.
|
||||
- Message delivery confirmation (with sender opt-in or opt-out per contact, TBC).
|
||||
- Privately share your location.
|
||||
- Feeds/broadcasts.
|
||||
- Web widgets for custom interactivity in the chats.
|
||||
- Programmable chat automations / rules (automatic replies/forward/deletion/sending, reminders, etc.).
|
||||
- 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:
|
||||
- keep all your contacts and groups even if you lose the domain.
|
||||
- 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).
|
||||
- High capacity multi-node SMP relays.
|
||||
|
||||
|
||||
20
apps/android/.gitignore
vendored
Normal file
20
apps/android/.gitignore
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
*.iml
|
||||
.gradle
|
||||
/local.properties
|
||||
/.idea/caches
|
||||
/.idea/libraries
|
||||
/.idea/modules.xml
|
||||
/.idea/workspace.xml
|
||||
/.idea/navEditor.xml
|
||||
/.idea/assetWizardSettings.xml
|
||||
/.idea/deploymentTargetDropDown.xml
|
||||
/.idea/misc.xml
|
||||
/.idea/uiDesigner.xml
|
||||
/.idea/kotlinc.xml
|
||||
.DS_Store
|
||||
/build
|
||||
/captures
|
||||
.externalNativeBuild
|
||||
.cxx
|
||||
local.properties
|
||||
app/src/main/cpp/libs/
|
||||
1
apps/android/.idea/.name
generated
Normal file
1
apps/android/.idea/.name
generated
Normal file
@@ -0,0 +1 @@
|
||||
SimpleX
|
||||
6
apps/android/.idea/compiler.xml
generated
Normal file
6
apps/android/.idea/compiler.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="CompilerConfiguration">
|
||||
<bytecodeTargetLevel target="11" />
|
||||
</component>
|
||||
</project>
|
||||
19
apps/android/.idea/gradle.xml
generated
Normal file
19
apps/android/.idea/gradle.xml
generated
Normal file
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="GradleMigrationSettings" migrationVersion="1" />
|
||||
<component name="GradleSettings">
|
||||
<option name="linkedExternalProjectsSettings">
|
||||
<GradleProjectSettings>
|
||||
<option name="testRunner" value="GRADLE" />
|
||||
<option name="distributionType" value="DEFAULT_WRAPPED" />
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||
<option name="modules">
|
||||
<set>
|
||||
<option value="$PROJECT_DIR$" />
|
||||
<option value="$PROJECT_DIR$/app" />
|
||||
</set>
|
||||
</option>
|
||||
</GradleProjectSettings>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
20
apps/android/.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
20
apps/android/.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
@@ -0,0 +1,20 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="PreviewAnnotationInFunctionWithParameters" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewMultipleParameterProviders" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
</profile>
|
||||
</component>
|
||||
6
apps/android/.idea/vcs.xml
generated
Normal file
6
apps/android/.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$/../.." vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -4,6 +4,7 @@ This readme is currently a stub and as such is in development.
|
||||
|
||||
Ultimately, this readme will act as a guide to contributing to the develop of the SimpleX android app.
|
||||
|
||||
|
||||
## Gotchas
|
||||
|
||||
#### 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`.
|
||||
|
||||
Note that this is not an issue for the app store build of the app as this is signed with our app store credentials and thus there is a stable signature over users. Developers do not have general access to these credentials for development and testing.
|
||||
|
||||
## Adding icons
|
||||
|
||||
1. Find a [Material symbol](https://fonts.google.com/icons?icon.style=Rounded) in Rounded category.
|
||||
|
||||
2. Set weight to 400, grade to -25 and size to 48px.
|
||||
|
||||
3. Click on the icon, choose Android and download XML file.
|
||||
|
||||
4. Update the color to black (#FF000000) and the size to "24.dp", as in other icons.
|
||||
|
||||
For example, this is [add reaction icon](https://fonts.google.com/icons?selected=Material+Symbols+Rounded:add_reaction:FILL@0;wght@300;GRAD@-25;opsz@24&icon.style=Rounded).
|
||||
235
apps/android/app/build.gradle
Normal file
235
apps/android/app/build.gradle
Normal file
@@ -0,0 +1,235 @@
|
||||
plugins {
|
||||
id 'com.android.application'
|
||||
id 'org.jetbrains.kotlin.android'
|
||||
id 'org.jetbrains.kotlin.plugin.serialization'
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdk 32
|
||||
|
||||
defaultConfig {
|
||||
applicationId "chat.simplex.app"
|
||||
minSdk 26
|
||||
targetSdk 32
|
||||
versionCode 117
|
||||
versionName "5.0"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables {
|
||||
useSupportLibrary true
|
||||
}
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
cppFlags ''
|
||||
}
|
||||
}
|
||||
manifestPlaceholders.app_name = "@string/app_name"
|
||||
manifestPlaceholders.provider_authorities = "chat.simplex.app.provider"
|
||||
manifestPlaceholders.extract_native_libs = compression_level != "0"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
debug {
|
||||
applicationIdSuffix "$application_id_suffix"
|
||||
debuggable new Boolean("$enable_debuggable")
|
||||
manifestPlaceholders.app_name = "$app_name"
|
||||
// Provider can't be the same for different apps on the same device
|
||||
manifestPlaceholders.provider_authorities = "chat.simplex.app${application_id_suffix}.provider"
|
||||
}
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = '1.8'
|
||||
freeCompilerArgs += "-opt-in=kotlinx.coroutines.DelicateCoroutinesApi"
|
||||
freeCompilerArgs += "-opt-in=androidx.compose.foundation.ExperimentalFoundationApi"
|
||||
freeCompilerArgs += "-opt-in=androidx.compose.ui.text.ExperimentalTextApi"
|
||||
freeCompilerArgs += "-opt-in=androidx.compose.material.ExperimentalMaterialApi"
|
||||
freeCompilerArgs += "-opt-in=com.google.accompanist.insets.ExperimentalAnimatedInsets"
|
||||
freeCompilerArgs += "-opt-in=com.google.accompanist.permissions.ExperimentalPermissionsApi"
|
||||
freeCompilerArgs += "-opt-in=kotlinx.serialization.InternalSerializationApi"
|
||||
freeCompilerArgs += "-opt-in=kotlinx.serialization.ExperimentalSerializationApi"
|
||||
}
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
path file('src/main/cpp/CMakeLists.txt')
|
||||
}
|
||||
}
|
||||
buildFeatures {
|
||||
compose true
|
||||
}
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion compose_version
|
||||
}
|
||||
packagingOptions {
|
||||
resources {
|
||||
excludes += '/META-INF/{AL2.0,LGPL2.1}'
|
||||
}
|
||||
jniLibs.useLegacyPackaging = compression_level != "0"
|
||||
}
|
||||
def isRelease = gradle.getStartParameter().taskNames.find({ it.toLowerCase().contains("release") }) != null
|
||||
def isBundle = gradle.getStartParameter().taskNames.find({ it.toLowerCase().contains("bundle") }) != null
|
||||
// if (isRelease) {
|
||||
// Comma separated list of languages that will be included in the apk
|
||||
android.defaultConfig.resConfigs(
|
||||
"en",
|
||||
"cs",
|
||||
"de",
|
||||
"es",
|
||||
"fr",
|
||||
"it",
|
||||
"nl",
|
||||
"pl",
|
||||
"ru",
|
||||
"zh-rCN"
|
||||
)
|
||||
// }
|
||||
if (isBundle) {
|
||||
defaultConfig.ndk.abiFilters 'arm64-v8a', 'armeabi-v7a'
|
||||
} else {
|
||||
splits {
|
||||
abi {
|
||||
enable true
|
||||
reset()
|
||||
if (isRelease) {
|
||||
include 'arm64-v8a', 'armeabi-v7a'
|
||||
} else {
|
||||
include 'arm64-v8a', 'armeabi-v7a'
|
||||
universalApk false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'androidx.core:core-ktx:1.7.0'
|
||||
implementation "androidx.compose.ui:ui:$compose_version"
|
||||
implementation "androidx.compose.material:material:$compose_version"
|
||||
implementation "androidx.compose.ui:ui-tooling-preview:$compose_version"
|
||||
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.4.1'
|
||||
implementation 'androidx.lifecycle:lifecycle-process:2.4.1'
|
||||
implementation 'androidx.activity:activity-compose:1.4.0'
|
||||
implementation 'androidx.fragment:fragment:1.4.1'
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-datetime:0.3.2'
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.2'
|
||||
implementation 'com.charleskorn.kaml:kaml:0.43.0'
|
||||
//implementation "androidx.compose.material:material-icons-extended:$compose_version"
|
||||
implementation "androidx.compose.ui:ui-util:$compose_version"
|
||||
implementation "androidx.navigation:navigation-compose:2.4.1"
|
||||
implementation "com.google.accompanist:accompanist-insets:0.23.0"
|
||||
implementation 'androidx.webkit:webkit:1.4.0'
|
||||
implementation "com.godaddy.android.colorpicker:compose-color-picker:0.4.2"
|
||||
|
||||
def work_version = "2.7.1"
|
||||
implementation "androidx.work:work-runtime-ktx:$work_version"
|
||||
implementation "androidx.work:work-multiprocess:$work_version"
|
||||
|
||||
def camerax_version = "1.1.0-beta01"
|
||||
implementation "androidx.camera:camera-core:${camerax_version}"
|
||||
implementation "androidx.camera:camera-camera2:${camerax_version}"
|
||||
implementation "androidx.camera:camera-lifecycle:${camerax_version}"
|
||||
implementation "androidx.camera:camera-view:${camerax_version}"
|
||||
|
||||
//Barcode
|
||||
implementation 'org.boofcv:boofcv-android:0.40.1'
|
||||
implementation 'org.boofcv:boofcv-core:0.40.1'
|
||||
|
||||
//Camera Permission
|
||||
implementation "com.google.accompanist:accompanist-permissions:0.23.0"
|
||||
implementation "com.google.accompanist:accompanist-pager:0.25.1"
|
||||
|
||||
// Link Previews
|
||||
implementation 'org.jsoup:jsoup:1.13.1'
|
||||
|
||||
// Biometric authentication
|
||||
implementation 'androidx.biometric:biometric:1.2.0-alpha04'
|
||||
|
||||
// GIFs support
|
||||
implementation "io.coil-kt:coil-compose:2.1.0"
|
||||
implementation "io.coil-kt:coil-gif:2.1.0"
|
||||
|
||||
// Video support
|
||||
implementation "com.google.android.exoplayer:exoplayer:2.17.1"
|
||||
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
|
||||
androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version"
|
||||
debugImplementation "androidx.compose.ui:ui-tooling:$compose_version"
|
||||
}
|
||||
|
||||
// Don't do anything if no compression is needed
|
||||
if (compression_level != "0") {
|
||||
tasks.whenTaskAdded { task ->
|
||||
if (task.name == 'packageDebug') {
|
||||
task.finalizedBy compressApk
|
||||
} else if (task.name == 'packageRelease') {
|
||||
task.finalizedBy compressApk
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register("compressApk") {
|
||||
doLast {
|
||||
def isRelease = gradle.getStartParameter().taskNames.find({ it.toLowerCase().contains("release") }) != null
|
||||
def buildType
|
||||
if (isRelease) {
|
||||
buildType = "release"
|
||||
} else {
|
||||
buildType = "debug"
|
||||
}
|
||||
def javaHome = System.properties['java.home'] ?: org.gradle.internal.jvm.Jvm.current().getJavaHome()
|
||||
def sdkDir = android.getSdkDirectory().getAbsolutePath()
|
||||
def keyAlias = ""
|
||||
def keyPassword = ""
|
||||
def storeFile = ""
|
||||
def storePassword = ""
|
||||
if (project.properties['android.injected.signing.key.alias'] != null) {
|
||||
keyAlias = project.properties['android.injected.signing.key.alias']
|
||||
keyPassword = project.properties['android.injected.signing.key.password']
|
||||
storeFile = project.properties['android.injected.signing.store.file']
|
||||
storePassword = project.properties['android.injected.signing.store.password']
|
||||
} else if (android.signingConfigs.hasProperty(buildType)) {
|
||||
def gradleConfig = android.signingConfigs[buildType]
|
||||
keyAlias = gradleConfig.keyAlias
|
||||
keyPassword = gradleConfig.keyPassword
|
||||
storeFile = gradleConfig.storeFile
|
||||
storePassword = gradleConfig.storePassword
|
||||
} else {
|
||||
// There is no signing config for current build type, can't sign the apk
|
||||
println("No signing configs for this build type: $buildType")
|
||||
return
|
||||
}
|
||||
|
||||
def outputDir = tasks["package${buildType.capitalize()}"].outputs.files.last()
|
||||
|
||||
exec {
|
||||
workingDir '../../../scripts/android'
|
||||
setEnvironment(['JAVA_HOME': "$javaHome"])
|
||||
commandLine './compress-and-sign-apk.sh', \
|
||||
"$compression_level", \
|
||||
"$outputDir", \
|
||||
"$sdkDir", \
|
||||
"$storeFile", \
|
||||
"$storePassword", \
|
||||
"$keyAlias", \
|
||||
"$keyPassword"
|
||||
}
|
||||
|
||||
if (project.properties['android.injected.signing.key.alias'] != null && buildType == 'release') {
|
||||
new File(outputDir, "app-release.apk").renameTo(new File(outputDir, "simplex.apk"))
|
||||
new File(outputDir, "app-armeabi-v7a-release.apk").renameTo(new File(outputDir, "simplex-armv7a.apk"))
|
||||
new File(outputDir, "app-arm64-v8a-release.apk").renameTo(new File(outputDir, "simplex.apk"))
|
||||
}
|
||||
|
||||
// View all gradle properties set
|
||||
// project.properties.each { k, v -> println "$k -> $v" }
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,6 @@
|
||||
<uses-feature android:name="android.hardware.camera" />
|
||||
|
||||
<uses-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.RECORD_AUDIO" />
|
||||
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
|
||||
@@ -64,4 +64,4 @@ target_link_libraries( # Specifies the target library.
|
||||
|
||||
# Links the target library to the log library
|
||||
# included in the NDK.
|
||||
${log-lib})
|
||||
${log-lib})
|
||||
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
@@ -10,19 +10,23 @@ import android.view.WindowManager
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.viewModels
|
||||
import androidx.compose.animation.core.*
|
||||
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.graphics.Color
|
||||
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.fragment.app.FragmentActivity
|
||||
import androidx.lifecycle.*
|
||||
import chat.simplex.app.MainActivity.Companion.enteredBackground
|
||||
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.views.SplashView
|
||||
import chat.simplex.app.views.call.ActiveCallView
|
||||
@@ -32,17 +36,12 @@ import chat.simplex.app.views.chatlist.*
|
||||
import chat.simplex.app.views.database.DatabaseErrorView
|
||||
import chat.simplex.app.views.helpers.*
|
||||
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.newchat.*
|
||||
import chat.simplex.app.views.onboarding.*
|
||||
import chat.simplex.app.views.usersettings.LAMode
|
||||
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.flow.distinctUntilChanged
|
||||
import java.lang.ref.WeakReference
|
||||
|
||||
class MainActivity: FragmentActivity() {
|
||||
companion object {
|
||||
@@ -62,11 +61,9 @@ class MainActivity: FragmentActivity() {
|
||||
}
|
||||
}
|
||||
private val vm by viewModels<SimplexViewModel>()
|
||||
private val destroyedAfterBackPress = mutableStateOf(false)
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
SimplexApp.context.mainActivity = WeakReference(this)
|
||||
// testJson()
|
||||
val m = vm.chatModel
|
||||
applyAppLocale(m.controller.appPrefs.appLanguage)
|
||||
@@ -86,17 +83,14 @@ class MainActivity: FragmentActivity() {
|
||||
}
|
||||
setContent {
|
||||
SimpleXTheme {
|
||||
Surface(color = MaterialTheme.colors.background) {
|
||||
MainPage(
|
||||
m,
|
||||
userAuthorized,
|
||||
laFailed,
|
||||
destroyedAfterBackPress,
|
||||
::runAuthenticate,
|
||||
::setPerformLA,
|
||||
showLANotice = { showLANotice(m.controller.appPrefs.laNoticeShown) }
|
||||
)
|
||||
}
|
||||
MainPage(
|
||||
m,
|
||||
userAuthorized,
|
||||
laFailed,
|
||||
::runAuthenticate,
|
||||
::setPerformLA,
|
||||
showLANotice = { showLANotice(m.controller.appPrefs.laNoticeShown, this) }
|
||||
)
|
||||
}
|
||||
}
|
||||
SimplexApp.context.schedulePeriodicServiceRestartWorker()
|
||||
@@ -114,12 +108,7 @@ class MainActivity: FragmentActivity() {
|
||||
val enteredBackgroundVal = enteredBackground.value
|
||||
val delay = vm.chatModel.controller.appPrefs.laLockDelay.get()
|
||||
if (enteredBackgroundVal == null || elapsedRealtime() - enteredBackgroundVal >= delay * 1000) {
|
||||
if (userAuthorized.value != false) {
|
||||
/** [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()
|
||||
}
|
||||
runAuthenticate()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,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
|
||||
clearAuthState()
|
||||
laFailed.value = true
|
||||
destroyedAfterBackPress.value = true
|
||||
}
|
||||
if (!onBackPressedDispatcher.hasEnabledCallbacks()) {
|
||||
// Drop shared content
|
||||
@@ -161,28 +149,27 @@ class MainActivity: FragmentActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun setAuthState() {
|
||||
userAuthorized.value = !vm.chatModel.controller.appPrefs.performLA.get()
|
||||
}
|
||||
|
||||
private fun runAuthenticate() {
|
||||
val m = vm.chatModel
|
||||
setAuthState()
|
||||
if (userAuthorized.value == false) {
|
||||
if (!m.controller.appPrefs.performLA.get()) {
|
||||
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
|
||||
CoroutineScope(Dispatchers.Default).launch {
|
||||
delay(50)
|
||||
withContext(Dispatchers.Main) {
|
||||
authenticate(
|
||||
if (m.controller.appPrefs.laMode.get() == LAMode.SYSTEM)
|
||||
generalGetString(MR.strings.auth_unlock)
|
||||
generalGetString(R.string.auth_unlock)
|
||||
else
|
||||
generalGetString(MR.strings.la_enter_app_passcode),
|
||||
generalGetString(R.string.la_enter_app_passcode),
|
||||
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
|
||||
generalGetString(MR.strings.auth_unlock),
|
||||
selfDestruct = true,
|
||||
generalGetString(R.string.auth_unlock),
|
||||
this@MainActivity,
|
||||
completed = { laResult ->
|
||||
when (laResult) {
|
||||
LAResult.Success ->
|
||||
@@ -208,49 +195,50 @@ class MainActivity: FragmentActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun showLANotice(laNoticeShown: SharedPreference<Boolean>) {
|
||||
private fun showLANotice(laNoticeShown: SharedPreference<Boolean>, activity: FragmentActivity) {
|
||||
Log.d(TAG, "showLANotice")
|
||||
if (!laNoticeShown.get()) {
|
||||
laNoticeShown.set(true)
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(MR.strings.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),
|
||||
confirmText = generalGetString(MR.strings.la_notice_turn_on),
|
||||
title = generalGetString(R.string.la_notice_title_simplex_lock),
|
||||
text = generalGetString(R.string.la_notice_to_protect_your_information_turn_on_simplex_lock_you_will_be_prompted_to_complete_authentication_before_this_feature_is_enabled),
|
||||
confirmText = generalGetString(R.string.la_notice_turn_on),
|
||||
onConfirm = {
|
||||
withBGApi { // to remove this call, change ordering of onConfirm call in AlertManager
|
||||
showChooseLAMode(laNoticeShown)
|
||||
showChooseLAMode(laNoticeShown, activity)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showChooseLAMode(laNoticeShown: SharedPreference<Boolean>) {
|
||||
private fun showChooseLAMode(laNoticeShown: SharedPreference<Boolean>, activity: FragmentActivity) {
|
||||
Log.d(TAG, "showLANotice")
|
||||
laNoticeShown.set(true)
|
||||
AlertManager.shared.showAlertDialogStacked(
|
||||
title = generalGetString(MR.strings.la_lock_mode),
|
||||
title = generalGetString(R.string.la_lock_mode),
|
||||
text = null,
|
||||
confirmText = generalGetString(MR.strings.la_lock_mode_passcode),
|
||||
dismissText = generalGetString(MR.strings.la_lock_mode_system),
|
||||
confirmText = generalGetString(R.string.la_lock_mode_passcode),
|
||||
dismissText = generalGetString(R.string.la_lock_mode_system),
|
||||
onConfirm = {
|
||||
AlertManager.shared.hideAlert()
|
||||
setPasscode()
|
||||
},
|
||||
onDismiss = {
|
||||
AlertManager.shared.hideAlert()
|
||||
initialEnableLA()
|
||||
initialEnableLA(activity)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun initialEnableLA() {
|
||||
private fun initialEnableLA(activity: FragmentActivity) {
|
||||
val m = vm.chatModel
|
||||
val appPrefs = m.controller.appPrefs
|
||||
m.controller.appPrefs.laMode.set(LAMode.SYSTEM)
|
||||
authenticate(
|
||||
generalGetString(MR.strings.auth_enable_simplex_lock),
|
||||
generalGetString(MR.strings.auth_confirm_credential),
|
||||
generalGetString(R.string.auth_enable_simplex_lock),
|
||||
generalGetString(R.string.auth_confirm_credential),
|
||||
activity,
|
||||
completed = { laResult ->
|
||||
when (laResult) {
|
||||
LAResult.Success -> {
|
||||
@@ -291,32 +279,32 @@ class MainActivity: FragmentActivity() {
|
||||
appPrefs.performLA.set(false)
|
||||
laPasscodeNotSetAlert()
|
||||
},
|
||||
close = close
|
||||
)
|
||||
close)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setPerformLA(on: Boolean) {
|
||||
private fun setPerformLA(on: Boolean, activity: FragmentActivity) {
|
||||
vm.chatModel.controller.appPrefs.laNoticeShown.set(true)
|
||||
if (on) {
|
||||
enableLA()
|
||||
enableLA(activity)
|
||||
} else {
|
||||
disableLA()
|
||||
disableLA(activity)
|
||||
}
|
||||
}
|
||||
|
||||
private fun enableLA() {
|
||||
private fun enableLA(activity: FragmentActivity) {
|
||||
val m = vm.chatModel
|
||||
authenticate(
|
||||
if (m.controller.appPrefs.laMode.get() == LAMode.SYSTEM)
|
||||
generalGetString(MR.strings.auth_enable_simplex_lock)
|
||||
generalGetString(R.string.auth_enable_simplex_lock)
|
||||
else
|
||||
generalGetString(MR.strings.new_passcode),
|
||||
generalGetString(R.string.new_passcode),
|
||||
if (m.controller.appPrefs.laMode.get() == LAMode.SYSTEM)
|
||||
generalGetString(MR.strings.auth_confirm_credential)
|
||||
generalGetString(R.string.auth_confirm_credential)
|
||||
else
|
||||
"",
|
||||
activity,
|
||||
completed = { laResult ->
|
||||
val prefPerformLA = m.controller.appPrefs.performLA
|
||||
when (laResult) {
|
||||
@@ -341,27 +329,25 @@ class MainActivity: FragmentActivity() {
|
||||
)
|
||||
}
|
||||
|
||||
private fun disableLA() {
|
||||
private fun disableLA(activity: FragmentActivity) {
|
||||
val m = vm.chatModel
|
||||
authenticate(
|
||||
if (m.controller.appPrefs.laMode.get() == LAMode.SYSTEM)
|
||||
generalGetString(MR.strings.auth_disable_simplex_lock)
|
||||
generalGetString(R.string.auth_disable_simplex_lock)
|
||||
else
|
||||
generalGetString(MR.strings.la_enter_app_passcode),
|
||||
generalGetString(R.string.la_enter_app_passcode),
|
||||
if (m.controller.appPrefs.laMode.get() == LAMode.SYSTEM)
|
||||
generalGetString(MR.strings.auth_confirm_credential)
|
||||
generalGetString(R.string.auth_confirm_credential)
|
||||
else
|
||||
generalGetString(MR.strings.auth_disable_simplex_lock),
|
||||
generalGetString(R.string.auth_disable_simplex_lock),
|
||||
activity,
|
||||
completed = { laResult ->
|
||||
val prefPerformLA = m.controller.appPrefs.performLA
|
||||
val selfDestructPref = m.controller.appPrefs.selfDestruct
|
||||
when (laResult) {
|
||||
LAResult.Success -> {
|
||||
m.performLA.value = false
|
||||
prefPerformLA.set(false)
|
||||
ksAppPassword.remove()
|
||||
selfDestructPref.set(false)
|
||||
ksSelfDestructPassword.remove()
|
||||
}
|
||||
is LAResult.Failed -> { /* Can be called multiple times on every failure */ }
|
||||
is LAResult.Error -> {
|
||||
@@ -390,9 +376,8 @@ fun MainPage(
|
||||
chatModel: ChatModel,
|
||||
userAuthorized: MutableState<Boolean?>,
|
||||
laFailed: MutableState<Boolean>,
|
||||
destroyedAfterBackPress: MutableState<Boolean>,
|
||||
runAuthenticate: () -> Unit,
|
||||
setPerformLA: (Boolean) -> Unit,
|
||||
setPerformLA: (Boolean, FragmentActivity) -> Unit,
|
||||
showLANotice: () -> Unit
|
||||
) {
|
||||
var showChatDatabaseError by rememberSaveable {
|
||||
@@ -427,38 +412,43 @@ fun MainPage(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AuthView() {
|
||||
Surface(color = MaterialTheme.colors.background) {
|
||||
Box(
|
||||
Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
SimpleButton(
|
||||
stringResource(MR.strings.auth_unlock),
|
||||
icon = painterResource(MR.images.ic_lock),
|
||||
click = {
|
||||
laFailed.value = false
|
||||
runAuthenticate()
|
||||
}
|
||||
)
|
||||
}
|
||||
fun authView() {
|
||||
Box(
|
||||
Modifier.fillMaxSize().background(MaterialTheme.colors.background),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
SimpleButton(
|
||||
stringResource(R.string.auth_unlock),
|
||||
icon = painterResource(R.drawable.ic_lock),
|
||||
click = {
|
||||
laFailed.value = false
|
||||
runAuthenticate()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Box {
|
||||
val onboarding = chatModel.onboardingStage.value
|
||||
val userCreated = chatModel.userCreated.value
|
||||
var showInitializationView by remember { mutableStateOf(false) }
|
||||
when {
|
||||
chatModel.chatDbStatus.value == null && showInitializationView -> InitializationView()
|
||||
showChatDatabaseError -> {
|
||||
chatModel.chatDbStatus.value?.let {
|
||||
DatabaseErrorView(chatModel.chatDbStatus, chatModel.controller.appPrefs)
|
||||
}
|
||||
}
|
||||
onboarding == null || userCreated == null -> SplashView()
|
||||
userAuthorized.value != true -> {
|
||||
if (chatModel.controller.appPrefs.performLA.get() && laFailed.value) {
|
||||
authView()
|
||||
} else {
|
||||
SplashView()
|
||||
}
|
||||
}
|
||||
onboarding == OnboardingStage.OnboardingComplete && userCreated -> {
|
||||
Box {
|
||||
if (chatModel.showCallView.value) ActiveCallView(chatModel)
|
||||
else {
|
||||
showAdvertiseLAAlert = true
|
||||
BoxWithConstraints {
|
||||
var currentChatId by rememberSaveable { mutableStateOf(chatModel.chatId.value) }
|
||||
@@ -504,41 +494,16 @@ fun MainPage(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
onboarding == OnboardingStage.Step1_SimpleXInfo -> SimpleXInfo(chatModel, onboarding = true)
|
||||
onboarding == OnboardingStage.Step2_CreateProfile -> CreateProfile(chatModel) {}
|
||||
onboarding == OnboardingStage.Step3_CreateSimpleXAddress -> CreateSimpleXAddress(chatModel)
|
||||
onboarding == OnboardingStage.Step4_SetNotificationsMode -> SetNotificationsMode(chatModel)
|
||||
onboarding == OnboardingStage.Step3_SetNotificationsMode -> SetNotificationsMode(chatModel)
|
||||
}
|
||||
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
|
||||
if (invitation != null) IncomingCallAlertView(invitation, chatModel)
|
||||
AlertManager.shared.showInView()
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
delay(1000)
|
||||
if (chatModel.chatDbStatus.value == null) {
|
||||
showInitializationView = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DisposableEffectOnRotate {
|
||||
@@ -550,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) {
|
||||
val userId = getUserIdFromIntent(intent)
|
||||
when (intent?.action) {
|
||||
@@ -580,7 +529,7 @@ fun processNotificationIntent(intent: Intent?, chatModel: ChatModel) {
|
||||
}
|
||||
val cInfo = chatModel.getChat(chatId)?.chatInfo
|
||||
chatModel.clearOverlays.value = true
|
||||
if (cInfo != null && (cInfo is ChatInfo.Direct || cInfo is ChatInfo.Group)) openChat(cInfo, chatModel)
|
||||
if (cInfo != null) openChat(cInfo, chatModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -602,7 +551,7 @@ fun processNotificationIntent(intent: Intent?, chatModel: ChatModel) {
|
||||
chatModel.clearOverlays.value = true
|
||||
val invitation = chatModel.callInvitations[chatId]
|
||||
if (invitation == null) {
|
||||
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.call_already_ended))
|
||||
AlertManager.shared.showAlertMsg(generalGetString(R.string.call_already_ended))
|
||||
} else {
|
||||
chatModel.callManager.acceptIncomingCall(invitation = invitation)
|
||||
}
|
||||
@@ -674,17 +623,17 @@ fun connectIfOpenedViaUri(uri: Uri, chatModel: ChatModel) {
|
||||
} else {
|
||||
withUriAction(uri) { linkType ->
|
||||
val title = when (linkType) {
|
||||
ConnectionLinkType.CONTACT -> generalGetString(MR.strings.connect_via_contact_link)
|
||||
ConnectionLinkType.INVITATION -> generalGetString(MR.strings.connect_via_invitation_link)
|
||||
ConnectionLinkType.GROUP -> generalGetString(MR.strings.connect_via_group_link)
|
||||
ConnectionLinkType.CONTACT -> generalGetString(R.string.connect_via_contact_link)
|
||||
ConnectionLinkType.INVITATION -> generalGetString(R.string.connect_via_invitation_link)
|
||||
ConnectionLinkType.GROUP -> generalGetString(R.string.connect_via_group_link)
|
||||
}
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = title,
|
||||
text = if (linkType == ConnectionLinkType.GROUP)
|
||||
generalGetString(MR.strings.you_will_join_group)
|
||||
generalGetString(R.string.you_will_join_group)
|
||||
else
|
||||
generalGetString(MR.strings.profile_will_be_sent_to_contact_sending_link),
|
||||
confirmText = generalGetString(MR.strings.connect_via_link_verb),
|
||||
generalGetString(R.string.profile_will_be_sent_to_contact_sending_link),
|
||||
confirmText = generalGetString(R.string.connect_via_link_verb),
|
||||
onConfirm = {
|
||||
withApi {
|
||||
Log.d(TAG, "connectIfOpenedViaUri: connecting")
|
||||
@@ -10,11 +10,9 @@ import chat.simplex.app.ui.theme.DefaultTheme
|
||||
import chat.simplex.app.views.helpers.*
|
||||
import chat.simplex.app.views.onboarding.OnboardingStage
|
||||
import chat.simplex.app.views.usersettings.NotificationsMode
|
||||
import com.jakewharton.processphoenix.ProcessPhoenix
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import java.io.*
|
||||
import java.lang.ref.WeakReference
|
||||
import java.util.*
|
||||
import java.util.concurrent.Semaphore
|
||||
import java.util.concurrent.TimeUnit
|
||||
@@ -38,20 +36,15 @@ external fun chatParseServer(str: String): String
|
||||
external fun chatPasswordHash(pwd: String, salt: String): String
|
||||
|
||||
class SimplexApp: Application(), LifecycleEventObserver {
|
||||
var mainActivity: WeakReference<MainActivity> = WeakReference(null)
|
||||
val chatModel: ChatModel
|
||||
get() = chatController.chatModel
|
||||
val appPreferences: AppPreferences
|
||||
get() = chatController.appPrefs
|
||||
lateinit var chatController: ChatController
|
||||
|
||||
val chatController: ChatController = ChatController
|
||||
var isAppOnForeground: Boolean = false
|
||||
|
||||
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 dbAbsolutePathPrefix = getFilesDirectory()
|
||||
val dbAbsolutePathPrefix = getFilesDirectory(SimplexApp.context)
|
||||
val confirm = confirmMigrations ?: if (appPreferences.confirmDBUpgrades.get()) MigrationConfirmation.Error else MigrationConfirmation.YesUp
|
||||
val migrated: Array<Any> = chatMigrateInit(dbAbsolutePathPrefix, dbKey, confirm.value)
|
||||
val res: DBMigrationResult = kotlin.runCatching {
|
||||
@@ -60,7 +53,11 @@ class SimplexApp: Application(), LifecycleEventObserver {
|
||||
val ctrl = if (res is DBMigrationResult.OK) {
|
||||
migrated[1] as Long
|
||||
} else null
|
||||
chatController.ctrl = ctrl
|
||||
if (::chatController.isInitialized) {
|
||||
chatController.ctrl = ctrl
|
||||
} else {
|
||||
chatController = ChatController(ctrl, ntfManager, applicationContext, appPreferences)
|
||||
}
|
||||
chatModel.chatDbEncrypted.value = dbKey != ""
|
||||
chatModel.chatDbStatus.value = res
|
||||
if (res != DBMigrationResult.OK) {
|
||||
@@ -68,43 +65,38 @@ class SimplexApp: Application(), LifecycleEventObserver {
|
||||
} else if (startChat) {
|
||||
// If we migrated successfully means previous re-encryption process on database level finished successfully too
|
||||
if (appPreferences.encryptionStartedAt.get() != null) appPreferences.encryptionStartedAt.set(null)
|
||||
val user = chatController.apiGetActiveUser()
|
||||
if (user == null) {
|
||||
chatModel.controller.appPrefs.onboardingStage.set(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
|
||||
withApi {
|
||||
val user = chatController.apiGetActiveUser()
|
||||
if (user == null) {
|
||||
chatModel.onboardingStage.value = OnboardingStage.Step1_SimpleXInfo
|
||||
} else {
|
||||
savedOnboardingStage
|
||||
}
|
||||
chatController.startChat(user)
|
||||
// Prevents from showing "Enable notifications" alert when onboarding wasn't complete yet
|
||||
if (chatModel.onboardingStage.value == OnboardingStage.OnboardingComplete) {
|
||||
SimplexService.showBackgroundServiceNoticeIfNeeded()
|
||||
chatController.startChat(user)
|
||||
chatController.showBackgroundServiceNoticeIfNeeded()
|
||||
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() {
|
||||
super.onCreate()
|
||||
if (ProcessPhoenix.isPhoenixProcess(this)) {
|
||||
return;
|
||||
}
|
||||
context = this
|
||||
initChatController()
|
||||
ProcessLifecycleOwner.get().lifecycle.addObserver(this)
|
||||
context.getDir("temp", MODE_PRIVATE).deleteRecursively()
|
||||
withBGApi {
|
||||
initChatController()
|
||||
runMigrations()
|
||||
}
|
||||
ProcessLifecycleOwner.get().lifecycle.addObserver(this@SimplexApp)
|
||||
runMigrations()
|
||||
}
|
||||
|
||||
override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
|
||||
@@ -134,7 +126,7 @@ class SimplexApp: Application(), LifecycleEventObserver {
|
||||
Lifecycle.Event.ON_RESUME -> {
|
||||
isAppOnForeground = true
|
||||
if (chatModel.onboardingStage.value == OnboardingStage.OnboardingComplete) {
|
||||
SimplexService.showBackgroundServiceNoticeIfNeeded()
|
||||
chatController.showBackgroundServiceNoticeIfNeeded()
|
||||
}
|
||||
/**
|
||||
* We're starting service here instead of in [Lifecycle.Event.ON_START] because
|
||||
@@ -145,7 +137,7 @@ class SimplexApp: Application(), LifecycleEventObserver {
|
||||
chatModel.onboardingStage.value == OnboardingStage.OnboardingComplete &&
|
||||
appPreferences.notificationsMode.get() == NotificationsMode.SERVICE.name
|
||||
) {
|
||||
SimplexService.start()
|
||||
SimplexService.start(applicationContext)
|
||||
}
|
||||
}
|
||||
else -> isAppOnForeground = false
|
||||
@@ -155,12 +147,12 @@ class SimplexApp: Application(), LifecycleEventObserver {
|
||||
|
||||
fun allowToStartServiceAfterAppExit() = with(chatModel.controller) {
|
||||
appPrefs.notificationsMode.get() == NotificationsMode.SERVICE.name &&
|
||||
(!NotificationsMode.SERVICE.requiresIgnoringBattery || SimplexService.isIgnoringBatteryOptimizations())
|
||||
(!NotificationsMode.SERVICE.requiresIgnoringBattery || isIgnoringBatteryOptimizations(chatModel.controller.appContext))
|
||||
}
|
||||
|
||||
private fun allowToStartPeriodically() = with(chatModel.controller) {
|
||||
appPrefs.notificationsMode.get() == NotificationsMode.PERIODIC.name &&
|
||||
(!NotificationsMode.PERIODIC.requiresIgnoringBattery || SimplexService.isIgnoringBatteryOptimizations())
|
||||
(!NotificationsMode.PERIODIC.requiresIgnoringBattery || isIgnoringBatteryOptimizations(chatModel.controller.appContext))
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -1,29 +1,17 @@
|
||||
package chat.simplex.app
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.*
|
||||
import android.content.*
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.os.*
|
||||
import android.provider.Settings
|
||||
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.content.ContextCompat
|
||||
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.usersettings.NotificationsMode
|
||||
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.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
// based on:
|
||||
// https://robertohuertas.com/2019/06/29/android_foreground_services/
|
||||
@@ -53,8 +41,8 @@ class SimplexService: Service() {
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
Log.d(TAG, "Simplex service created")
|
||||
val title = generalGetString(MR.strings.simplex_service_notification_title)
|
||||
val text = generalGetString(MR.strings.simplex_service_notification_text)
|
||||
val title = getString(R.string.simplex_service_notification_title)
|
||||
val text = getString(R.string.simplex_service_notification_text)
|
||||
notificationManager = createNotificationChannel()
|
||||
serviceNotification = createNotification(title, text)
|
||||
startForeground(SIMPLEX_SERVICE_ID, serviceNotification)
|
||||
@@ -98,7 +86,6 @@ class SimplexService: Service() {
|
||||
isStartingService = true
|
||||
withApi {
|
||||
val chatController = (application as SimplexApp).chatController
|
||||
waitDbMigrationEnds(chatController)
|
||||
try {
|
||||
Log.w(TAG, "Starting foreground service")
|
||||
val chatDbStatus = chatController.chatModel.chatDbStatus.value
|
||||
@@ -154,7 +141,7 @@ class SimplexService: Service() {
|
||||
setupIntent.putExtra(Settings.EXTRA_APP_PACKAGE, packageName)
|
||||
setupIntent.putExtra(Settings.EXTRA_CHANNEL_ID, NOTIFICATION_CHANNEL_ID)
|
||||
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()
|
||||
@@ -178,6 +165,7 @@ class SimplexService: Service() {
|
||||
it.setPackage(packageName)
|
||||
};
|
||||
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;
|
||||
alarmService.set(AlarmManager.ELAPSED_REALTIME, SystemClock.elapsedRealtime() + 1000, restartServicePendingIntent);
|
||||
}
|
||||
@@ -218,7 +206,7 @@ class SimplexService: Service() {
|
||||
}
|
||||
if (getServiceState(context) == ServiceState.STARTED) {
|
||||
Log.d(TAG, "ServiceStartWorker: Starting foreground service (work ID: $id)")
|
||||
start()
|
||||
start(context)
|
||||
}
|
||||
return Result.success()
|
||||
}
|
||||
@@ -259,7 +247,7 @@ class SimplexService: Service() {
|
||||
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
|
||||
@@ -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}")
|
||||
withContext(Dispatchers.IO) {
|
||||
Intent(SimplexApp.context, SimplexService::class.java).also {
|
||||
Intent(context, SimplexService::class.java).also {
|
||||
it.action = action.name
|
||||
ContextCompat.startForegroundService(SimplexApp.context, it)
|
||||
ContextCompat.startForegroundService(context, it)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -307,15 +295,15 @@ class SimplexService: Service() {
|
||||
}
|
||||
|
||||
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
|
||||
else -> generalGetString(MR.strings.database_initialization_error_title)
|
||||
else -> generalGetString(R.string.database_initialization_error_title)
|
||||
}
|
||||
|
||||
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
|
||||
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)
|
||||
@@ -336,172 +324,6 @@ class SimplexService: Service() {
|
||||
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)
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,48 +1,45 @@
|
||||
package chat.simplex.app.model
|
||||
|
||||
import android.Manifest
|
||||
import android.app.*
|
||||
import android.app.TaskStackBuilder
|
||||
import android.content.*
|
||||
import android.content.pm.PackageManager
|
||||
import android.graphics.BitmapFactory
|
||||
import android.hardware.display.DisplayManager
|
||||
import android.media.AudioAttributes
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
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.views.call.*
|
||||
import chat.simplex.app.views.chatlist.acceptContactRequest
|
||||
import chat.simplex.app.views.helpers.*
|
||||
import chat.simplex.app.views.usersettings.NotificationPreviewMode
|
||||
import chat.simplex.res.MR
|
||||
import kotlinx.datetime.Clock
|
||||
|
||||
object NtfManager {
|
||||
const val MessageChannel: String = "chat.simplex.app.MESSAGE_NOTIFICATION"
|
||||
const val MessageGroup: String = "chat.simplex.app.MESSAGE_NOTIFICATION"
|
||||
const val OpenChatAction: String = "chat.simplex.app.OPEN_CHAT"
|
||||
const val ShowChatsAction: String = "chat.simplex.app.SHOW_CHATS"
|
||||
class NtfManager(val context: Context, private val appPreferences: AppPreferences) {
|
||||
companion object {
|
||||
const val MessageChannel: String = "chat.simplex.app.MESSAGE_NOTIFICATION"
|
||||
const val MessageGroup: String = "chat.simplex.app.MESSAGE_NOTIFICATION"
|
||||
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
|
||||
const val CallChannel: String = "chat.simplex.app.CALL_NOTIFICATION_1"
|
||||
const val AcceptCallAction: String = "chat.simplex.app.ACCEPT_CALL"
|
||||
const val RejectCallAction: String = "chat.simplex.app.REJECT_CALL"
|
||||
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
|
||||
// DO NOT change notification channel settings / names
|
||||
const val CallChannel: String = "chat.simplex.app.CALL_NOTIFICATION_1"
|
||||
const val AcceptCallAction: String = "chat.simplex.app.ACCEPT_CALL"
|
||||
const val RejectCallAction: String = "chat.simplex.app.REJECT_CALL"
|
||||
const val CallNotificationId: Int = -1
|
||||
|
||||
fun getUserIdFromIntent(intent: Intent?): Long? {
|
||||
val userId = intent?.getLongExtra(UserIdKey, -1L)
|
||||
return if (userId == -1L || userId == null) null else userId
|
||||
private const val UserIdKey: String = "userId"
|
||||
private const val ChatIdKey: String = "chatId"
|
||||
|
||||
fun getUserIdFromIntent(intent: Intent?): Long? {
|
||||
val userId = intent?.getLongExtra(UserIdKey, -1L)
|
||||
return if (userId == -1L || userId == null) null else userId
|
||||
}
|
||||
}
|
||||
|
||||
private val manager: NotificationManager = 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 val msgNtfTimeoutMs = 30000L
|
||||
|
||||
@@ -61,7 +58,7 @@ object NtfManager {
|
||||
.setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
|
||||
.build()
|
||||
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.enableVibration(true)
|
||||
// the numbers below are explained here: https://developer.android.com/reference/android/os/Vibrator
|
||||
@@ -73,8 +70,8 @@ object NtfManager {
|
||||
fun cancelNotificationsForChat(chatId: String) {
|
||||
prevNtfTime.remove(chatId)
|
||||
manager.cancel(chatId.hashCode())
|
||||
val msgNtfs = manager.activeNotifications.filter { ntf ->
|
||||
ntf.notification.channelId == MessageChannel
|
||||
val msgNtfs = manager.activeNotifications.filter {
|
||||
ntf -> ntf.notification.channelId == MessageChannel
|
||||
}
|
||||
if (msgNtfs.count() == 1) {
|
||||
// Have a group notification with no children so cancel it
|
||||
@@ -87,7 +84,7 @@ object NtfManager {
|
||||
user = user,
|
||||
chatId = cInfo.id,
|
||||
displayName = cInfo.displayName,
|
||||
msgText = generalGetString(MR.strings.notification_new_contact_request),
|
||||
msgText = generalGetString(R.string.notification_new_contact_request),
|
||||
image = cInfo.image,
|
||||
listOf(NotificationAction.ACCEPT_CONTACT_REQUEST)
|
||||
)
|
||||
@@ -98,7 +95,7 @@ object NtfManager {
|
||||
user = user,
|
||||
chatId = contact.id,
|
||||
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 recentNotification = (now - prevNtfTime.getOrDefault(chatId, 0) < msgNtfTimeoutMs)
|
||||
prevNtfTime[chatId] = now
|
||||
|
||||
val previewMode = appPreferences.notificationPreviewMode.get()
|
||||
val title = if (previewMode == NotificationPreviewMode.HIDDEN.name) generalGetString(MR.strings.notification_preview_somebody) else displayName
|
||||
val content = if (previewMode != NotificationPreviewMode.MESSAGE.name) generalGetString(MR.strings.notification_preview_new_message) else msgText
|
||||
val title = if (previewMode == NotificationPreviewMode.HIDDEN.name) generalGetString(R.string.notification_preview_somebody) else displayName
|
||||
val content = if (previewMode != NotificationPreviewMode.MESSAGE.name) generalGetString(R.string.notification_preview_new_message) else msgText
|
||||
val largeIcon = when {
|
||||
actions.isEmpty() -> null
|
||||
image == null || previewMode == NotificationPreviewMode.HIDDEN.name -> BitmapFactory.decodeResource(context.resources, R.drawable.icon)
|
||||
@@ -143,10 +141,11 @@ object NtfManager {
|
||||
actionIntent.putExtra(ChatIdKey, chatId)
|
||||
val actionPendingIntent: PendingIntent = PendingIntent.getBroadcast(SimplexApp.context, 0, actionIntent, flags)
|
||||
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)
|
||||
}
|
||||
|
||||
val summary = NotificationCompat.Builder(context, MessageChannel)
|
||||
.setSmallIcon(R.drawable.ntf_icon)
|
||||
.setColor(0x88FFFF)
|
||||
@@ -158,17 +157,14 @@ object NtfManager {
|
||||
|
||||
with(NotificationManagerCompat.from(context)) {
|
||||
// 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(0, summary)
|
||||
}
|
||||
notify(chatId.hashCode(), builder.build())
|
||||
notify(0, summary)
|
||||
}
|
||||
}
|
||||
|
||||
fun notifyCallInvitation(invitation: RcvCallInvitation) {
|
||||
val keyguardManager = getKeyguardManager(context)
|
||||
Log.d(
|
||||
TAG,
|
||||
Log.d(TAG,
|
||||
"notifyCallInvitation pre-requests: " +
|
||||
"keyguard locked ${keyguardManager.isKeyguardLocked}, " +
|
||||
"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)
|
||||
NotificationCompat.Builder(context, CallChannel)
|
||||
.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(MR.strings.reject), chatPendingIntent(RejectCallAction, invitation.user.userId, contactId, true))
|
||||
.addAction(R.drawable.ntf_icon, generalGetString(R.string.accept), chatPendingIntent(AcceptCallAction, invitation.user.userId, contactId))
|
||||
.addAction(R.drawable.ntf_icon, generalGetString(R.string.reject), chatPendingIntent(RejectCallAction, invitation.user.userId, contactId, true))
|
||||
.setFullScreenIntent(fullScreenPendingIntent, true)
|
||||
.setSound(soundUri)
|
||||
}
|
||||
val text = generalGetString(
|
||||
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 {
|
||||
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 title = if (previewMode == NotificationPreviewMode.HIDDEN.name)
|
||||
generalGetString(MR.strings.notification_preview_somebody)
|
||||
generalGetString(R.string.notification_preview_somebody)
|
||||
else
|
||||
invitation.contact.displayName
|
||||
val largeIcon = if (image == null || previewMode == NotificationPreviewMode.HIDDEN.name)
|
||||
@@ -227,9 +223,7 @@ object NtfManager {
|
||||
// This makes notification sound and vibration repeat endlessly
|
||||
notification.flags = notification.flags or NotificationCompat.FLAG_INSISTENT
|
||||
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)
|
||||
}
|
||||
|
||||
fun cancelAllNotifications() {
|
||||
manager.cancelAll()
|
||||
}
|
||||
|
||||
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
|
||||
return if (md != null) {
|
||||
var res = ""
|
||||
@@ -282,8 +272,8 @@ object NtfManager {
|
||||
* old ones if needed
|
||||
* */
|
||||
fun createNtfChannelsMaybeShowAlert() {
|
||||
manager.createNotificationChannel(NotificationChannel(MessageChannel, generalGetString(MR.strings.ntf_channel_messages), NotificationManager.IMPORTANCE_HIGH))
|
||||
manager.createNotificationChannel(callNotificationChannel(CallChannel, generalGetString(MR.strings.ntf_channel_calls)))
|
||||
manager.createNotificationChannel(NotificationChannel(MessageChannel, generalGetString(R.string.ntf_channel_messages), NotificationManager.IMPORTANCE_HIGH))
|
||||
manager.createNotificationChannel(callNotificationChannel(CallChannel, generalGetString(R.string.ntf_channel_calls)))
|
||||
// Remove old channels since they can't be edited
|
||||
manager.deleteNotificationChannel("chat.simplex.app.CALL_NOTIFICATION")
|
||||
manager.deleteNotificationChannel("chat.simplex.app.LOCK_SCREEN_CALL_NOTIFICATION")
|
||||
@@ -308,16 +298,14 @@ object NtfManager {
|
||||
}
|
||||
val apiId = chatId.replace("<@", "").toLongOrNull() ?: return
|
||||
acceptContactRequest(apiId, cInfo, isCurrentUser, m)
|
||||
cancelNotificationsForChat(chatId)
|
||||
m.controller.ntfManager.cancelNotificationsForChat(chatId)
|
||||
}
|
||||
|
||||
RejectCallAction -> {
|
||||
val invitation = m.callInvitations[chatId]
|
||||
if (invitation != null) {
|
||||
m.callManager.endCall(invitation = invitation)
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
Log.e(TAG, "Unknown action. Make sure you provide action from NotificationAction enum")
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -21,6 +21,7 @@ val ToolbarDark = Color(80, 80, 80, 12)
|
||||
val SettingsSecondaryLight = Color(200, 196, 195, 90)
|
||||
val GroupDark = Color(80, 80, 80, 60)
|
||||
val IncomingCallLight = Color(239, 237, 236, 255)
|
||||
val IncomingCallDark = Color(34, 30, 29, 255)
|
||||
val WarningOrange = Color(255, 127, 0, 255)
|
||||
val WarningYellow = Color(255, 192, 0, 255)
|
||||
val FileLight = Color(183, 190, 199, 255)
|
||||
@@ -13,7 +13,6 @@ import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.SimplexApp
|
||||
import chat.simplex.app.views.helpers.*
|
||||
import chat.simplex.res.MR
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
@@ -70,15 +69,15 @@ enum class ThemeColor {
|
||||
|
||||
val text: String
|
||||
get() = when (this) {
|
||||
PRIMARY -> generalGetString(MR.strings.color_primary)
|
||||
PRIMARY_VARIANT -> generalGetString(MR.strings.color_primary_variant)
|
||||
SECONDARY -> generalGetString(MR.strings.color_secondary)
|
||||
SECONDARY_VARIANT -> generalGetString(MR.strings.color_secondary_variant)
|
||||
BACKGROUND -> generalGetString(MR.strings.color_background)
|
||||
SURFACE -> generalGetString(MR.strings.color_surface)
|
||||
TITLE -> generalGetString(MR.strings.color_title)
|
||||
SENT_MESSAGE -> generalGetString(MR.strings.color_sent_message)
|
||||
RECEIVED_MESSAGE -> generalGetString(MR.strings.color_received_message)
|
||||
PRIMARY -> generalGetString(R.string.color_primary)
|
||||
PRIMARY_VARIANT -> generalGetString(R.string.color_primary_variant)
|
||||
SECONDARY -> generalGetString(R.string.color_secondary)
|
||||
SECONDARY_VARIANT -> generalGetString(R.string.color_secondary_variant)
|
||||
BACKGROUND -> generalGetString(R.string.color_background)
|
||||
SURFACE -> generalGetString(R.string.color_surface)
|
||||
TITLE -> generalGetString(R.string.color_title)
|
||||
SENT_MESSAGE -> generalGetString(R.string.color_sent_message)
|
||||
RECEIVED_MESSAGE -> generalGetString(R.string.color_received_message)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,29 +128,11 @@ data class ThemeColors(
|
||||
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 =
|
||||
Color(this.replace("#", "").toLongOrNull(16) ?: Color.White.toArgb().toLong())
|
||||
|
||||
private fun Color.toReadableHex(): String = "#" + Integer.toHexString(toArgb())
|
||||
|
||||
@Serializable
|
||||
data class ThemeOverrides (
|
||||
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 {
|
||||
return if (baseTheme == DefaultTheme.SIMPLEX) {
|
||||
this.background(brush = Brush.linearGradient(
|
||||
@@ -7,7 +7,7 @@ import chat.simplex.app.R
|
||||
import chat.simplex.app.SimplexApp
|
||||
import chat.simplex.app.model.AppPreferences
|
||||
import chat.simplex.app.views.helpers.generalGetString
|
||||
import chat.simplex.res.MR
|
||||
import okhttp3.internal.toHexString
|
||||
|
||||
object ThemeManager {
|
||||
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))
|
||||
}
|
||||
|
||||
fun currentThemeOverridesForExport(darkForSystemTheme: Boolean): ThemeOverrides {
|
||||
val themeName = appPrefs.currentTheme.get()!!
|
||||
val nonSystemThemeName = if (themeName != DefaultTheme.SYSTEM.name) {
|
||||
themeName
|
||||
} else {
|
||||
if (darkForSystemTheme) appPrefs.systemDarkTheme.get()!! else DefaultTheme.LIGHT.name
|
||||
}
|
||||
val overrides = appPrefs.themeOverrides.get().toMutableMap()
|
||||
val nonFilledTheme = overrides[nonSystemThemeName] ?: ThemeOverrides(base = CurrentColors.value.base, colors = ThemeColors())
|
||||
return nonFilledTheme.copy(colors = nonFilledTheme.colors.withFilledColors(CurrentColors.value.base))
|
||||
fun currentThemeData(darkForSystemTheme: Boolean): ThemeData {
|
||||
val t = currentColors(darkForSystemTheme)
|
||||
return ThemeData(colors = ThemeColors(
|
||||
primary = t.colors.primary.toReadableHex(),
|
||||
primaryVariant = t.colors.primaryVariant.toReadableHex(),
|
||||
secondary = t.colors.secondary.toReadableHex(),
|
||||
secondaryVariant = t.colors.secondaryVariant.toReadableHex(),
|
||||
background = t.colors.background.toReadableHex(),
|
||||
surface = t.colors.surface.toReadableHex(),
|
||||
title = t.appColors.title.toReadableHex(),
|
||||
sentMessage = t.appColors.sentMessage.toReadableHex(),
|
||||
receivedMessage = t.appColors.receivedMessage.toReadableHex()
|
||||
))
|
||||
}
|
||||
|
||||
// colors, default theme enum, localized name of theme
|
||||
@@ -63,28 +66,28 @@ object ThemeManager {
|
||||
Triple(
|
||||
if (darkForSystemTheme) systemDarkThemeColors().first else LightColorPalette,
|
||||
DefaultTheme.SYSTEM,
|
||||
generalGetString(MR.strings.theme_system)
|
||||
generalGetString(R.string.theme_system)
|
||||
)
|
||||
)
|
||||
allThemes.add(
|
||||
Triple(
|
||||
LightColorPalette,
|
||||
DefaultTheme.LIGHT,
|
||||
generalGetString(MR.strings.theme_light)
|
||||
generalGetString(R.string.theme_light)
|
||||
)
|
||||
)
|
||||
allThemes.add(
|
||||
Triple(
|
||||
DarkColorPalette,
|
||||
DefaultTheme.DARK,
|
||||
generalGetString(MR.strings.theme_dark)
|
||||
generalGetString(R.string.theme_dark)
|
||||
)
|
||||
)
|
||||
allThemes.add(
|
||||
Triple(
|
||||
SimplexColorPalette,
|
||||
DefaultTheme.SIMPLEX,
|
||||
generalGetString(MR.strings.theme_simplex)
|
||||
generalGetString(R.string.theme_simplex)
|
||||
)
|
||||
)
|
||||
return allThemes
|
||||
@@ -125,12 +128,11 @@ object ThemeManager {
|
||||
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 prevValue = overrides[theme.base.name] ?: ThemeOverrides(base = CurrentColors.value.base, colors = ThemeColors())
|
||||
overrides[theme.base.name] = prevValue.copy(colors = theme.colors)
|
||||
val prevValue = overrides[name] ?: ThemeOverrides(base = CurrentColors.value.base, colors = ThemeColors())
|
||||
overrides[name] = prevValue.copy(colors = theme.colors)
|
||||
appPrefs.themeOverrides.set(overrides)
|
||||
appPrefs.currentTheme.set(theme.base.name)
|
||||
CurrentColors.value = currentColors(!CurrentColors.value.colors.isLight)
|
||||
}
|
||||
|
||||
@@ -149,4 +151,4 @@ object ThemeManager {
|
||||
}
|
||||
}
|
||||
|
||||
private fun Color.toReadableHex(): String = "#" + Integer.toHexString(toArgb())
|
||||
private fun Color.toReadableHex(): String = "#" + toArgb().toHexString()
|
||||
@@ -4,16 +4,16 @@ import androidx.compose.material.Typography
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.*
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.res.MR
|
||||
import chat.simplex.app.R
|
||||
|
||||
// https://github.com/rsms/inter
|
||||
val Inter: FontFamily = FontFamily(
|
||||
Font(MR.fonts.Inter.regular.fontResourceId),
|
||||
Font(MR.fonts.Inter.italic.fontResourceId, style = FontStyle.Italic),
|
||||
Font(MR.fonts.Inter.bold.fontResourceId, FontWeight.Bold),
|
||||
Font(MR.fonts.Inter.semibold.fontResourceId, FontWeight.SemiBold),
|
||||
Font(MR.fonts.Inter.medium.fontResourceId, FontWeight.Medium),
|
||||
Font(MR.fonts.Inter.light.fontResourceId, FontWeight.Light)
|
||||
val Inter = FontFamily(
|
||||
Font(R.font.inter_regular),
|
||||
Font(R.font.inter_italic, style = FontStyle.Italic),
|
||||
Font(R.font.inter_bold, weight = FontWeight.Bold),
|
||||
Font(R.font.inter_semi_bold, weight = FontWeight.SemiBold),
|
||||
Font(R.font.inter_medium, weight = FontWeight.Medium),
|
||||
Font(R.font.inter_light, weight = FontWeight.Light),
|
||||
)
|
||||
|
||||
// Set of Material typography styles to start with
|
||||
@@ -33,11 +33,6 @@ val Typography = Typography(
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 18.5.sp
|
||||
),
|
||||
h4 = TextStyle(
|
||||
fontFamily = Inter,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 17.5.sp
|
||||
),
|
||||
body1 = TextStyle(
|
||||
fontFamily = Inter,
|
||||
fontWeight = FontWeight.Normal,
|
||||
@@ -14,7 +14,7 @@ fun SplashView() {
|
||||
color = MaterialTheme.colors.background
|
||||
) {
|
||||
// Image(
|
||||
// painter = painterResource(MR.images.logo),
|
||||
// painter = painterResource(R.drawable.logo),
|
||||
// contentDescription = "Simplex Icon",
|
||||
// modifier = Modifier
|
||||
// .height(230.dp)
|
||||
@@ -86,7 +86,7 @@ fun TerminalLayout(
|
||||
userIsObserver = false,
|
||||
userCanSend = true,
|
||||
allowVoiceToContact = {},
|
||||
sendMessage = { sendCommand() },
|
||||
sendMessage = sendCommand,
|
||||
sendLiveMessage = null,
|
||||
updateLiveMessage = null,
|
||||
onMessageChange = ::onMessageChange,
|
||||
@@ -99,8 +99,8 @@ fun TerminalLayout(
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.padding(contentPadding)
|
||||
.fillMaxWidth(),
|
||||
color = MaterialTheme.colors.background
|
||||
.fillMaxWidth()
|
||||
.themedBackground()
|
||||
) {
|
||||
TerminalLog(terminalItems)
|
||||
}
|
||||
@@ -128,7 +128,7 @@ fun TerminalLog(terminalItems: List<TerminalItem>) {
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
ModalManager.shared.showModal(endButtons = { ShareButton { shareText(item.details) } }) {
|
||||
ModalManager.shared.showModal(endButtons = { ShareButton { shareText(context, item.details) } }) {
|
||||
SelectionContainer(modifier = Modifier.verticalScroll(rememberScrollState())) {
|
||||
Text(item.details, modifier = Modifier.padding(horizontal = DEFAULT_PADDING).padding(bottom = DEFAULT_PADDING))
|
||||
}
|
||||
@@ -14,8 +14,8 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.*
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||
@@ -31,8 +31,6 @@ import chat.simplex.app.views.helpers.*
|
||||
import chat.simplex.app.views.onboarding.OnboardingStage
|
||||
import chat.simplex.app.views.onboarding.ReadableText
|
||||
import com.google.accompanist.insets.navigationBarsWithImePadding
|
||||
import chat.simplex.res.MR
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
|
||||
@@ -46,75 +44,77 @@ fun CreateProfilePanel(chatModel: ChatModel, close: () -> Unit) {
|
||||
val fullName = rememberSaveable { mutableStateOf("") }
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState())
|
||||
) {
|
||||
/*CloseSheetBar(close = {
|
||||
if (chatModel.users.isEmpty()) {
|
||||
chatModel.onboardingStage.value = OnboardingStage.Step1_SimpleXInfo
|
||||
} else {
|
||||
close()
|
||||
}
|
||||
})*/
|
||||
Column(Modifier.padding(horizontal = DEFAULT_PADDING)) {
|
||||
AppBarTitle(stringResource(MR.strings.create_profile), bottomPadding = DEFAULT_PADDING)
|
||||
ReadableText(MR.strings.your_profile_is_stored_on_your_device, TextAlign.Center, padding = PaddingValues(), style = MaterialTheme.typography.body1)
|
||||
ReadableText(MR.strings.profile_is_only_shared_with_your_contacts, TextAlign.Center, style = MaterialTheme.typography.body1)
|
||||
Spacer(Modifier.height(DEFAULT_PADDING))
|
||||
Row(Modifier.padding(bottom = DEFAULT_PADDING_HALF).fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
|
||||
Text(
|
||||
stringResource(MR.strings.display_name),
|
||||
fontSize = 16.sp
|
||||
)
|
||||
if (!isValidDisplayName(displayName.value)) {
|
||||
Surface(Modifier.background(MaterialTheme.colors.onBackground)) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState())
|
||||
) {
|
||||
/*CloseSheetBar(close = {
|
||||
if (chatModel.users.isEmpty()) {
|
||||
chatModel.onboardingStage.value = OnboardingStage.Step1_SimpleXInfo
|
||||
} else {
|
||||
close()
|
||||
}
|
||||
})*/
|
||||
Column(Modifier.padding(horizontal = DEFAULT_PADDING * 1f)) {
|
||||
AppBarTitle(stringResource(R.string.create_profile))
|
||||
ReadableText(R.string.your_profile_is_stored_on_your_device, TextAlign.Center, padding = PaddingValues())
|
||||
ReadableText(R.string.profile_is_only_shared_with_your_contacts, TextAlign.Center)
|
||||
Spacer(Modifier.height(DEFAULT_PADDING * 1.5f))
|
||||
Row(Modifier.padding(bottom = DEFAULT_PADDING_HALF).fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
|
||||
Text(
|
||||
stringResource(MR.strings.no_spaces),
|
||||
fontSize = 16.sp,
|
||||
color = Color.Red
|
||||
stringResource(R.string.display_name),
|
||||
fontSize = 16.sp
|
||||
)
|
||||
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, "", ::isValidDisplayName)
|
||||
}
|
||||
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) {
|
||||
delay(300)
|
||||
focusRequester.requestFocus()
|
||||
LaunchedEffect(Unit) {
|
||||
delay(300)
|
||||
focusRequester.requestFocus()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -127,17 +127,13 @@ fun createProfile(chatModel: ChatModel, displayName: String, fullName: String, c
|
||||
chatModel.currentUser.value = user
|
||||
if (chatModel.users.isEmpty()) {
|
||||
chatModel.controller.startChat(user)
|
||||
chatModel.controller.appPrefs.onboardingStage.set(OnboardingStage.Step3_CreateSimpleXAddress)
|
||||
chatModel.onboardingStage.value = OnboardingStage.Step3_CreateSimpleXAddress
|
||||
chatModel.onboardingStage.value = OnboardingStage.Step3_SetNotificationsMode
|
||||
SimplexApp.context.chatModel.controller.ntfManager.createNtfChannelsMaybeShowAlert()
|
||||
} else {
|
||||
val users = chatModel.controller.listUsers()
|
||||
chatModel.users.clear()
|
||||
chatModel.users.addAll(users)
|
||||
chatModel.controller.getUserChatData()
|
||||
// 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()
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,7 @@ class CallManager(val chatModel: ChatModel) {
|
||||
}
|
||||
|
||||
fun acceptIncomingCall(invitation: RcvCallInvitation) {
|
||||
ModalManager.shared.closeModals()
|
||||
val call = chatModel.activeCall.value
|
||||
if (call == null) {
|
||||
justAcceptIncomingCall(invitation = invitation)
|
||||
@@ -13,6 +13,7 @@ import android.util.Log
|
||||
import android.view.ViewGroup
|
||||
import android.webkit.*
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.*
|
||||
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.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
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.usersettings.NotificationsMode
|
||||
import com.google.accompanist.permissions.rememberMultiplePermissionsState
|
||||
import chat.simplex.res.MR
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.encodeToString
|
||||
@@ -59,7 +58,7 @@ fun ActiveCallView(chatModel: ChatModel) {
|
||||
LaunchedEffect(Unit) {
|
||||
// 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
|
||||
if (!ntfModeService) SimplexService.start()
|
||||
if (!ntfModeService) SimplexService.start(SimplexApp.context)
|
||||
}
|
||||
DisposableEffect(Unit) {
|
||||
val am = SimplexApp.context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
||||
@@ -186,12 +185,10 @@ fun ActiveCallView(chatModel: ChatModel) {
|
||||
activity.volumeControlStream = AudioManager.STREAM_VOICE_CALL
|
||||
// Lock orientation to portrait in order to have good experience with calls
|
||||
activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
|
||||
chatModel.activeCallViewIsVisible.value = true
|
||||
onDispose {
|
||||
activity.volumeControlStream = prevVolumeControlStream
|
||||
// Unlock orientation
|
||||
activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
|
||||
chatModel.activeCallViewIsVisible.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -273,14 +270,14 @@ private fun ActiveCallOverlayLayout(
|
||||
ToggleAudioButton(call, toggleAudio)
|
||||
Spacer(Modifier.size(40.dp))
|
||||
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) {
|
||||
ControlButton(call, painterResource(MR.images.ic_flip_camera_android_filled), MR.strings.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_flip_camera_android_filled), R.string.icon_descr_flip_camera, flipCamera)
|
||||
ControlButton(call, painterResource(R.drawable.ic_videocam_filled), R.string.icon_descr_video_off, toggleVideo)
|
||||
} else {
|
||||
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(), contentAlignment = Alignment.Center) {
|
||||
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)) {
|
||||
@@ -316,7 +313,7 @@ private fun ActiveCallOverlayLayout(
|
||||
}
|
||||
|
||||
@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) {
|
||||
IconButton(onClick = action, enabled = enabled) {
|
||||
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
|
||||
private fun ToggleAudioButton(call: Call, toggleAudio: () -> Unit) {
|
||||
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 {
|
||||
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
|
||||
private fun ToggleSoundButton(call: Call, enabled: Boolean, toggleSound: () -> Unit) {
|
||||
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 {
|
||||
ControlButton(call, painterResource(MR.images.ic_volume_down), MR.strings.icon_descr_speaker_on, toggleSound, enabled)
|
||||
ControlButton(call, painterResource(R.drawable.ic_volume_down), R.string.icon_descr_speaker_on, toggleSound, enabled)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,8 +24,8 @@ import androidx.compose.ui.draw.scale
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -33,10 +33,9 @@ import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.app.*
|
||||
import chat.simplex.app.R
|
||||
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.views.helpers.ProfileImage
|
||||
import chat.simplex.res.MR
|
||||
import kotlinx.datetime.Clock
|
||||
|
||||
class IncomingCallActivity: ComponentActivity() {
|
||||
@@ -98,9 +97,8 @@ fun IncomingCallActivityView(m: ChatModel) {
|
||||
SimpleXTheme {
|
||||
Surface(
|
||||
Modifier
|
||||
.fillMaxSize(),
|
||||
color = MaterialTheme.colors.background
|
||||
) {
|
||||
.themedBackground()
|
||||
.fillMaxSize()) {
|
||||
if (showCallView) {
|
||||
Box {
|
||||
ActiveCallView(m)
|
||||
@@ -171,18 +169,18 @@ fun IncomingCallLockScreenAlertLayout(
|
||||
Text(invitation.contact.chatViewName, style = MaterialTheme.typography.h2)
|
||||
Spacer(Modifier.fillMaxHeight().weight(1f))
|
||||
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))
|
||||
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))
|
||||
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) {
|
||||
SimpleXLogo()
|
||||
Text(stringResource(MR.strings.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.open_simplex_chat_to_accept_call), textAlign = TextAlign.Center, lineHeight = 22.sp)
|
||||
Text(stringResource(R.string.allow_accepting_calls_from_lock_screen), textAlign = TextAlign.Center, style = MaterialTheme.typography.body2, lineHeight = 22.sp)
|
||||
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
|
||||
private fun SimpleXLogo() {
|
||||
Image(
|
||||
painter = painterResource(if (isInDarkTheme()) MR.images.logo_light else MR.images.logo),
|
||||
contentDescription = stringResource(MR.strings.image_descr_simplex_logo),
|
||||
painter = painterResource(if (isInDarkTheme()) R.drawable.logo_light else R.drawable.logo),
|
||||
contentDescription = stringResource(R.string.image_descr_simplex_logo),
|
||||
modifier = Modifier
|
||||
.padding(vertical = DEFAULT_PADDING)
|
||||
.fillMaxWidth(0.80f)
|
||||
@@ -228,9 +226,8 @@ fun PreviewIncomingCallLockScreenAlert() {
|
||||
SimpleXTheme(true) {
|
||||
Surface(
|
||||
Modifier
|
||||
.fillMaxSize(),
|
||||
color = MaterialTheme.colors.background
|
||||
) {
|
||||
.themedBackground()
|
||||
.fillMaxSize()) {
|
||||
IncomingCallLockScreenAlertLayout(
|
||||
invitation = RcvCallInvitation(
|
||||
user = User.sampleData,
|
||||
@@ -11,8 +11,8 @@ import androidx.compose.ui.draw.scale
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
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.R
|
||||
@@ -21,7 +21,6 @@ import chat.simplex.app.model.*
|
||||
import chat.simplex.app.ui.theme.*
|
||||
import chat.simplex.app.views.helpers.ProfileImage
|
||||
import chat.simplex.app.views.usersettings.ProfilePreview
|
||||
import chat.simplex.res.MR
|
||||
import kotlinx.datetime.Clock
|
||||
|
||||
@Composable
|
||||
@@ -29,7 +28,7 @@ fun IncomingCallAlertView(invitation: RcvCallInvitation, chatModel: ChatModel) {
|
||||
val cm = chatModel.callManager
|
||||
val cxt = LocalContext.current
|
||||
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() } }
|
||||
IncomingCallAlertLayout(
|
||||
invitation,
|
||||
@@ -51,18 +50,18 @@ fun IncomingCallAlertLayout(
|
||||
ignoreCall: () -> 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)) {
|
||||
IncomingCallInfo(invitation, chatModel)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween) {
|
||||
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) {
|
||||
CallButton(stringResource(MR.strings.reject), painterResource(MR.images.ic_call_end_filled), Color.Red, rejectCall)
|
||||
CallButton(stringResource(MR.strings.ignore), painterResource(MR.images.ic_close), MaterialTheme.colors.primary, ignoreCall)
|
||||
CallButton(stringResource(MR.strings.accept), painterResource(MR.images.ic_check_filled), SimplexGreen, acceptCall)
|
||||
CallButton(stringResource(R.string.reject), painterResource(R.drawable.ic_call_end_filled), Color.Red, rejectCall)
|
||||
CallButton(stringResource(R.string.ignore), painterResource(R.drawable.ic_close), MaterialTheme.colors.primary, ignoreCall)
|
||||
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)
|
||||
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))
|
||||
else CallIcon(painterResource(MR.images.ic_call_filled), stringResource(MR.strings.icon_descr_audio_call))
|
||||
if (invitation.callType.media == CallMediaType.Video) CallIcon(painterResource(R.drawable.ic_videocam_filled), stringResource(R.string.icon_descr_video_call))
|
||||
else CallIcon(painterResource(R.drawable.ic_call_filled), stringResource(R.string.icon_descr_audio_call))
|
||||
Spacer(Modifier.width(4.dp))
|
||||
Text(invitation.callTypeText, color = MaterialTheme.colors.onBackground)
|
||||
Text(invitation.callTypeText)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ class SoundPlayer {
|
||||
private var player: MediaPlayer? = null
|
||||
var playing = false
|
||||
|
||||
fun start(scope: CoroutineScope, sound: Boolean) {
|
||||
fun start(cxt: Context, scope: CoroutineScope, sound: Boolean) {
|
||||
player?.reset()
|
||||
player = MediaPlayer().apply {
|
||||
setAudioAttributes(
|
||||
@@ -28,7 +28,7 @@ class SoundPlayer {
|
||||
setDataSource(SimplexApp.context, Uri.parse("android.resource://" + SimplexApp.context.packageName + "/" + R.raw.ring_once))
|
||||
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)
|
||||
playing = true
|
||||
withScope(scope) {
|
||||
@@ -1,12 +1,13 @@
|
||||
package chat.simplex.app.views.call
|
||||
|
||||
import android.util.Log
|
||||
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.model.Contact
|
||||
import chat.simplex.app.model.User
|
||||
import chat.simplex.app.views.helpers.generalGetString
|
||||
import chat.simplex.res.MR
|
||||
import kotlinx.datetime.Instant
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
@@ -32,9 +33,9 @@ data class Call(
|
||||
|
||||
val encryptionStatus: String @Composable get() = when(callState) {
|
||||
CallState.WaitCapabilities -> ""
|
||||
CallState.InvitationSent -> stringResource(if (localEncrypted) MR.strings.status_e2e_encrypted else MR.strings.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)
|
||||
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)
|
||||
CallState.InvitationSent -> stringResource(if (localEncrypted) R.string.status_e2e_encrypted else R.string.status_no_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) 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
|
||||
@@ -52,15 +53,15 @@ enum class CallState {
|
||||
Ended;
|
||||
|
||||
val text: String @Composable get() = when(this) {
|
||||
WaitCapabilities -> stringResource(MR.strings.callstate_starting)
|
||||
InvitationSent -> stringResource(MR.strings.callstate_waiting_for_answer)
|
||||
InvitationAccepted -> stringResource(MR.strings.callstate_starting)
|
||||
OfferSent -> stringResource(MR.strings.callstate_waiting_for_confirmation)
|
||||
OfferReceived -> stringResource(MR.strings.callstate_received_answer)
|
||||
AnswerReceived -> stringResource(MR.strings.callstate_received_confirmation)
|
||||
Negotiated -> stringResource(MR.strings.callstate_connecting)
|
||||
Connected -> stringResource(MR.strings.callstate_connected)
|
||||
Ended -> stringResource(MR.strings.callstate_ended)
|
||||
WaitCapabilities -> stringResource(R.string.callstate_starting)
|
||||
InvitationSent -> stringResource(R.string.callstate_waiting_for_answer)
|
||||
InvitationAccepted -> stringResource(R.string.callstate_starting)
|
||||
OfferSent -> stringResource(R.string.callstate_waiting_for_confirmation)
|
||||
OfferReceived -> stringResource(R.string.callstate_received_answer)
|
||||
AnswerReceived -> stringResource(R.string.callstate_received_confirmation)
|
||||
Negotiated -> stringResource(R.string.callstate_connecting)
|
||||
Connected -> stringResource(R.string.callstate_connected)
|
||||
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 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) {
|
||||
CallMediaType.Video -> if (sharedKey == null) MR.strings.video_call_no_encryption else MR.strings.encrypted_video_call
|
||||
CallMediaType.Audio -> if (sharedKey == null) MR.strings.audio_call_no_encryption else MR.strings.encrypted_audio_call
|
||||
CallMediaType.Video -> if (sharedKey == null) R.string.video_call_no_encryption else R.string.encrypted_video_call
|
||||
CallMediaType.Audio -> if (sharedKey == null) R.string.audio_call_no_encryption else R.string.encrypted_audio_call
|
||||
})
|
||||
val callTitle: String get() = generalGetString(when(callType.media) {
|
||||
CallMediaType.Video -> MR.strings.incoming_video_call
|
||||
CallMediaType.Audio -> MR.strings.incoming_audio_call
|
||||
CallMediaType.Video -> R.string.incoming_video_call
|
||||
CallMediaType.Audio -> R.string.incoming_audio_call
|
||||
})
|
||||
}
|
||||
@Serializable data class CallCapabilities(val encryption: Boolean)
|
||||
@@ -113,9 +114,9 @@ sealed class WCallResponse {
|
||||
val remote = remoteCandidate?.candidateType
|
||||
return when {
|
||||
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 ->
|
||||
stringResource(MR.strings.call_connection_via_relay)
|
||||
stringResource(R.string.call_connection_via_relay)
|
||||
else ->
|
||||
"${local?.value ?: "unknown"} / ${remote?.value ?: "unknown"}"
|
||||
}
|
||||
@@ -6,7 +6,6 @@ import SectionBottomSpacer
|
||||
import SectionDividerSpaced
|
||||
import SectionItemView
|
||||
import SectionSpacer
|
||||
import SectionTextFooter
|
||||
import SectionView
|
||||
import android.widget.Toast
|
||||
import androidx.activity.compose.BackHandler
|
||||
@@ -19,9 +18,10 @@ 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.*
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
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
|
||||
@@ -29,13 +29,12 @@ 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.newchat.QRCode
|
||||
import chat.simplex.app.views.usersettings.*
|
||||
import chat.simplex.res.MR
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.datetime.Clock
|
||||
@@ -44,7 +43,7 @@ import kotlinx.datetime.Clock
|
||||
fun ChatInfoView(
|
||||
chatModel: ChatModel,
|
||||
contact: Contact,
|
||||
connectionStats: ConnectionStats?,
|
||||
connStats: ConnectionStats?,
|
||||
customUserProfile: Profile?,
|
||||
localAlias: String,
|
||||
connectionCode: String?,
|
||||
@@ -52,7 +51,6 @@ fun ChatInfoView(
|
||||
) {
|
||||
BackHandler(onBack = close)
|
||||
val chat = chatModel.chats.firstOrNull { it.id == chatModel.chatId.value }
|
||||
val connStats = remember { mutableStateOf(connectionStats) }
|
||||
val developerTools = chatModel.controller.appPrefs.developerTools.get()
|
||||
if (chat != null) {
|
||||
val contactNetworkStatus = remember(chatModel.networkStatuses.toMap()) {
|
||||
@@ -61,7 +59,7 @@ fun ChatInfoView(
|
||||
ChatInfoLayout(
|
||||
chat,
|
||||
contact,
|
||||
connStats = connStats,
|
||||
connStats,
|
||||
contactNetworkStatus.value,
|
||||
customUserProfile,
|
||||
localAlias,
|
||||
@@ -81,49 +79,7 @@ fun ChatInfoView(
|
||||
deleteContact = { deleteContactDialog(chat.chatInfo, chatModel, close) },
|
||||
clearChat = { clearChatDialog(chat.chatInfo, chatModel, close) },
|
||||
switchContactAddress = {
|
||||
showSwitchAddressAlert(switchAddress = {
|
||||
withApi {
|
||||
val cStats = chatModel.controller.apiSwitchContact(contact.contactId)
|
||||
connStats.value = cStats
|
||||
if (cStats != null) {
|
||||
chatModel.updateContactConnectionStats(contact, cStats)
|
||||
}
|
||||
close.invoke()
|
||||
}
|
||||
})
|
||||
},
|
||||
abortSwitchContactAddress = {
|
||||
showAbortSwitchAddressAlert(abortSwitchAddress = {
|
||||
withApi {
|
||||
val cStats = chatModel.controller.apiAbortSwitchContact(contact.contactId)
|
||||
connStats.value = cStats
|
||||
if (cStats != null) {
|
||||
chatModel.updateContactConnectionStats(contact, cStats)
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
syncContactConnection = {
|
||||
withApi {
|
||||
val cStats = chatModel.controller.apiSyncContactRatchet(contact.contactId, force = false)
|
||||
connStats.value = cStats
|
||||
if (cStats != null) {
|
||||
chatModel.updateContactConnectionStats(contact, cStats)
|
||||
}
|
||||
close.invoke()
|
||||
}
|
||||
},
|
||||
syncContactConnectionForce = {
|
||||
showSyncConnectionForceAlert(syncConnectionForce = {
|
||||
withApi {
|
||||
val cStats = chatModel.controller.apiSyncContactRatchet(contact.contactId, force = true)
|
||||
connStats.value = cStats
|
||||
if (cStats != null) {
|
||||
chatModel.updateContactConnectionStats(contact, cStats)
|
||||
}
|
||||
close.invoke()
|
||||
}
|
||||
})
|
||||
showSwitchContactAddressAlert(chatModel, contact.contactId)
|
||||
},
|
||||
verifyClicked = {
|
||||
ModalManager.shared.showModalCloseable { close ->
|
||||
@@ -156,9 +112,9 @@ fun ChatInfoView(
|
||||
|
||||
fun deleteContactDialog(chatInfo: ChatInfo, chatModel: ChatModel, close: (() -> Unit)? = null) {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(MR.strings.delete_contact_question),
|
||||
text = generalGetString(MR.strings.delete_contact_all_messages_deleted_cannot_undo_warning),
|
||||
confirmText = generalGetString(MR.strings.delete_verb),
|
||||
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)
|
||||
@@ -176,9 +132,9 @@ fun deleteContactDialog(chatInfo: ChatInfo, chatModel: ChatModel, close: (() ->
|
||||
|
||||
fun clearChatDialog(chatInfo: ChatInfo, chatModel: ChatModel, close: (() -> Unit)? = null) {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(MR.strings.clear_chat_question),
|
||||
text = generalGetString(MR.strings.clear_chat_warning),
|
||||
confirmText = generalGetString(MR.strings.clear_verb),
|
||||
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)
|
||||
@@ -197,7 +153,7 @@ fun clearChatDialog(chatInfo: ChatInfo, chatModel: ChatModel, close: (() -> Unit
|
||||
fun ChatInfoLayout(
|
||||
chat: Chat,
|
||||
contact: Contact,
|
||||
connStats: MutableState<ConnectionStats?>,
|
||||
connStats: ConnectionStats?,
|
||||
contactNetworkStatus: NetworkStatus,
|
||||
customUserProfile: Profile?,
|
||||
localAlias: String,
|
||||
@@ -208,12 +164,8 @@ fun ChatInfoLayout(
|
||||
deleteContact: () -> Unit,
|
||||
clearChat: () -> Unit,
|
||||
switchContactAddress: () -> Unit,
|
||||
abortSwitchContactAddress: () -> Unit,
|
||||
syncContactConnection: () -> Unit,
|
||||
syncContactConnectionForce: () -> Unit,
|
||||
verifyClicked: () -> Unit,
|
||||
) {
|
||||
val cStats = connStats.value
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
@@ -229,8 +181,8 @@ fun ChatInfoLayout(
|
||||
LocalAliasEditor(localAlias, updateValue = onLocalAliasChanged)
|
||||
SectionSpacer()
|
||||
if (customUserProfile != null) {
|
||||
SectionView(generalGetString(MR.strings.incognito).uppercase()) {
|
||||
InfoRow(generalGetString(MR.strings.incognito_random_profile), customUserProfile.chatViewName)
|
||||
SectionView(generalGetString(R.string.incognito).uppercase()) {
|
||||
InfoRow(generalGetString(R.string.incognito_random_profile), customUserProfile.chatViewName)
|
||||
}
|
||||
SectionDividerSpaced()
|
||||
}
|
||||
@@ -240,49 +192,27 @@ fun ChatInfoLayout(
|
||||
VerifyCodeButton(contact.verified, verifyClicked)
|
||||
}
|
||||
ContactPreferencesButton(openPreferences)
|
||||
if (cStats != null && cStats.ratchetSyncAllowed) {
|
||||
SynchronizeConnectionButton(syncContactConnection)
|
||||
} else if (developerTools) {
|
||||
SynchronizeConnectionButtonForce(syncContactConnectionForce)
|
||||
}
|
||||
}
|
||||
|
||||
SectionDividerSpaced()
|
||||
if (contact.contactLink != null) {
|
||||
SectionView(stringResource(MR.strings.address_section_title).uppercase()) {
|
||||
QRCode(contact.contactLink, Modifier.padding(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF).aspectRatio(1f))
|
||||
ShareAddressButton { shareText(contact.contactLink) }
|
||||
SectionTextFooter(stringResource(MR.strings.you_can_share_this_address_with_your_contacts).format(contact.displayName))
|
||||
}
|
||||
SectionDividerSpaced()
|
||||
}
|
||||
|
||||
SectionView(title = stringResource(MR.strings.conn_stats_section_title_servers)) {
|
||||
SectionItemView({
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(MR.strings.network_status),
|
||||
contactNetworkStatus.statusExplanation
|
||||
)}) {
|
||||
NetworkStatusRow(contactNetworkStatus)
|
||||
}
|
||||
if (cStats != null) {
|
||||
SwitchAddressButton(
|
||||
disabled = cStats.rcvQueuesInfo.any { it.rcvSwitchStatus != null } || cStats.ratchetSyncSendProhibited,
|
||||
switchAddress = switchContactAddress
|
||||
)
|
||||
if (cStats.rcvQueuesInfo.any { it.rcvSwitchStatus != null }) {
|
||||
AbortSwitchAddressButton(
|
||||
disabled = cStats.rcvQueuesInfo.any { it.rcvSwitchStatus != null && !it.canAbortSwitch } || cStats.ratchetSyncSendProhibited,
|
||||
abortSwitchAddress = abortSwitchContactAddress
|
||||
)
|
||||
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 = cStats.rcvQueuesInfo.map { it.rcvServer }
|
||||
if (rcvServers.isNotEmpty()) {
|
||||
SimplexServers(stringResource(MR.strings.receiving_via), rcvServers)
|
||||
val rcvServers = connStats.rcvServers
|
||||
if (rcvServers != null && rcvServers.isNotEmpty()) {
|
||||
SimplexServers(stringResource(R.string.receiving_via), rcvServers)
|
||||
}
|
||||
val sndServers = cStats.sndQueuesInfo.map { it.sndServer }
|
||||
if (sndServers.isNotEmpty()) {
|
||||
SimplexServers(stringResource(MR.strings.sending_via), sndServers)
|
||||
val sndServers = connStats.sndServers
|
||||
if (sndServers != null && sndServers.isNotEmpty()) {
|
||||
SimplexServers(stringResource(R.string.sending_via), sndServers)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -294,9 +224,9 @@ fun ChatInfoLayout(
|
||||
|
||||
if (developerTools) {
|
||||
SectionDividerSpaced()
|
||||
SectionView(title = stringResource(MR.strings.section_title_for_console)) {
|
||||
InfoRow(stringResource(MR.strings.info_row_local_name), chat.chatInfo.localDisplayName)
|
||||
InfoRow(stringResource(MR.strings.info_row_database_id), chat.chatInfo.apiId.toString())
|
||||
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()
|
||||
@@ -312,7 +242,7 @@ fun ChatInfoHeader(cInfo: ChatInfo, contact: Contact) {
|
||||
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(MR.images.ic_verified_user), null, Modifier.padding(end = 6.dp, top = 4.dp).size(24.dp), tint = MaterialTheme.colors.secondary)
|
||||
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),
|
||||
@@ -351,13 +281,13 @@ fun LocalAliasEditor(
|
||||
value,
|
||||
{
|
||||
Text(
|
||||
generalGetString(MR.strings.text_field_set_contact_placeholder),
|
||||
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(MR.images.ic_edit_filled), null, Modifier.padding(start = 7.dp)) }
|
||||
{ Icon(painterResource(R.drawable.ic_edit_filled), null, Modifier.padding(start = 7.dp)) }
|
||||
} else null,
|
||||
color = MaterialTheme.colors.secondary,
|
||||
focus = focus,
|
||||
@@ -392,10 +322,10 @@ private fun NetworkStatusRow(networkStatus: NetworkStatus) {
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
Text(stringResource(MR.strings.network_status))
|
||||
Text(stringResource(R.string.network_status))
|
||||
Icon(
|
||||
painterResource(MR.images.ic_info),
|
||||
stringResource(MR.strings.network_status),
|
||||
painterResource(R.drawable.ic_info),
|
||||
stringResource(R.string.network_status),
|
||||
tint = MaterialTheme.colors.primary
|
||||
)
|
||||
}
|
||||
@@ -418,12 +348,12 @@ private fun ServerImage(networkStatus: NetworkStatus) {
|
||||
Box(Modifier.size(18.dp)) {
|
||||
when (networkStatus) {
|
||||
is NetworkStatus.Connected ->
|
||||
Icon(painterResource(MR.images.ic_circle_filled), stringResource(MR.strings.icon_descr_server_status_connected), tint = Color.Green)
|
||||
Icon(painterResource(R.drawable.ic_circle_filled), stringResource(R.string.icon_descr_server_status_connected), tint = MaterialTheme.colors.primaryVariant)
|
||||
is NetworkStatus.Disconnected ->
|
||||
Icon(painterResource(MR.images.ic_pending_filled), stringResource(MR.strings.icon_descr_server_status_disconnected), tint = MaterialTheme.colors.secondary)
|
||||
Icon(painterResource(R.drawable.ic_pending_filled), stringResource(R.string.icon_descr_server_status_disconnected), tint = MaterialTheme.colors.secondary)
|
||||
is NetworkStatus.Error ->
|
||||
Icon(painterResource(MR.images.ic_error_filled), stringResource(MR.strings.icon_descr_server_status_error), tint = MaterialTheme.colors.secondary)
|
||||
else -> Icon(painterResource(MR.images.ic_circle), stringResource(MR.strings.icon_descr_server_status_pending), tint = MaterialTheme.colors.secondary)
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -434,57 +364,22 @@ fun SimplexServers(text: String, servers: List<String>) {
|
||||
val clipboardManager: ClipboardManager = LocalClipboardManager.current
|
||||
InfoRowEllipsis(text, info) {
|
||||
clipboardManager.setText(AnnotatedString(servers.joinToString(separator = ",")))
|
||||
Toast.makeText(SimplexApp.context, generalGetString(MR.strings.copied), Toast.LENGTH_SHORT).show()
|
||||
Toast.makeText(SimplexApp.context, generalGetString(R.string.copied), Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SwitchAddressButton(disabled: Boolean, switchAddress: () -> Unit) {
|
||||
SectionItemView(switchAddress) {
|
||||
Text(
|
||||
stringResource(MR.strings.switch_receiving_address),
|
||||
color = if (disabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary
|
||||
)
|
||||
fun SwitchAddressButton(onClick: () -> Unit) {
|
||||
SectionItemView(onClick) {
|
||||
Text(stringResource(R.string.switch_receiving_address), color = MaterialTheme.colors.primary)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AbortSwitchAddressButton(disabled: Boolean, abortSwitchAddress: () -> Unit) {
|
||||
SectionItemView(abortSwitchAddress) {
|
||||
Text(
|
||||
stringResource(MR.strings.abort_switch_receiving_address),
|
||||
color = if (disabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SynchronizeConnectionButton(syncConnection: () -> Unit) {
|
||||
SettingsActionItem(
|
||||
painterResource(MR.images.ic_sync_problem),
|
||||
stringResource(MR.strings.fix_connection),
|
||||
click = syncConnection,
|
||||
textColor = WarningOrange,
|
||||
iconColor = WarningOrange
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SynchronizeConnectionButtonForce(syncConnectionForce: () -> Unit) {
|
||||
SettingsActionItem(
|
||||
painterResource(MR.images.ic_warning),
|
||||
stringResource(MR.strings.renegotiate_encryption),
|
||||
click = syncConnectionForce,
|
||||
textColor = Color.Red,
|
||||
iconColor = Color.Red
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun VerifyCodeButton(contactVerified: Boolean, onClick: () -> Unit) {
|
||||
SettingsActionItem(
|
||||
if (contactVerified) painterResource(MR.images.ic_verified_user) else painterResource(MR.images.ic_shield),
|
||||
stringResource(if (contactVerified) MR.strings.view_security_code else MR.strings.verify_security_code),
|
||||
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,
|
||||
)
|
||||
@@ -493,8 +388,8 @@ fun VerifyCodeButton(contactVerified: Boolean, onClick: () -> Unit) {
|
||||
@Composable
|
||||
private fun ContactPreferencesButton(onClick: () -> Unit) {
|
||||
SettingsActionItem(
|
||||
painterResource(MR.images.ic_toggle_on),
|
||||
stringResource(MR.strings.contact_preferences),
|
||||
painterResource(R.drawable.ic_toggle_on),
|
||||
stringResource(R.string.contact_preferences),
|
||||
click = onClick
|
||||
)
|
||||
}
|
||||
@@ -502,8 +397,8 @@ private fun ContactPreferencesButton(onClick: () -> Unit) {
|
||||
@Composable
|
||||
fun ClearChatButton(onClick: () -> Unit) {
|
||||
SettingsActionItem(
|
||||
painterResource(MR.images.ic_settings_backup_restore),
|
||||
stringResource(MR.strings.clear_chat_button),
|
||||
painterResource(R.drawable.ic_settings_backup_restore),
|
||||
stringResource(R.string.clear_chat_button),
|
||||
click = onClick,
|
||||
textColor = WarningOrange,
|
||||
iconColor = WarningOrange,
|
||||
@@ -513,58 +408,34 @@ fun ClearChatButton(onClick: () -> Unit) {
|
||||
@Composable
|
||||
private fun DeleteContactButton(onClick: () -> Unit) {
|
||||
SettingsActionItem(
|
||||
painterResource(MR.images.ic_delete),
|
||||
stringResource(MR.strings.button_delete_contact),
|
||||
painterResource(R.drawable.ic_delete),
|
||||
stringResource(R.string.button_delete_contact),
|
||||
click = onClick,
|
||||
textColor = Color.Red,
|
||||
iconColor = Color.Red,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ShareAddressButton(onClick: () -> Unit) {
|
||||
SettingsActionItem(
|
||||
painterResource(MR.images.ic_share_filled),
|
||||
stringResource(MR.strings.share_address),
|
||||
onClick,
|
||||
iconColor = MaterialTheme.colors.primary,
|
||||
textColor = MaterialTheme.colors.primary,
|
||||
)
|
||||
}
|
||||
|
||||
private fun setContactAlias(contactApiId: Long, localAlias: String, chatModel: ChatModel) = withApi {
|
||||
chatModel.controller.apiSetContactAlias(contactApiId, localAlias)?.let {
|
||||
chatModel.updateContact(it)
|
||||
}
|
||||
}
|
||||
|
||||
fun showSwitchAddressAlert(switchAddress: () -> Unit) {
|
||||
private fun showSwitchContactAddressAlert(m: ChatModel, contactId: Long) {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(MR.strings.switch_receiving_address_question),
|
||||
text = generalGetString(MR.strings.switch_receiving_address_desc),
|
||||
confirmText = generalGetString(MR.strings.change_verb),
|
||||
onConfirm = switchAddress
|
||||
)
|
||||
}
|
||||
|
||||
fun showAbortSwitchAddressAlert(abortSwitchAddress: () -> Unit) {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(MR.strings.abort_switch_receiving_address_question),
|
||||
text = generalGetString(MR.strings.abort_switch_receiving_address_desc),
|
||||
confirmText = generalGetString(MR.strings.abort_switch_receiving_address_confirm),
|
||||
onConfirm = abortSwitchAddress,
|
||||
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,
|
||||
)
|
||||
}
|
||||
|
||||
fun showSyncConnectionForceAlert(syncConnectionForce: () -> Unit) {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(MR.strings.sync_connection_force_question),
|
||||
text = generalGetString(MR.strings.sync_connection_force_desc),
|
||||
confirmText = generalGetString(MR.strings.sync_connection_force_confirm),
|
||||
onConfirm = syncConnectionForce,
|
||||
destructive = true,
|
||||
)
|
||||
private fun switchContactAddress(m: ChatModel, contactId: Long) = withApi {
|
||||
m.controller.apiSwitchContact(contactId)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@@ -580,7 +451,7 @@ fun PreviewChatInfoLayout() {
|
||||
localAlias = "",
|
||||
connectionCode = "123",
|
||||
developerTools = false,
|
||||
connStats = remember { mutableStateOf(null) },
|
||||
connStats = null,
|
||||
contactNetworkStatus = NetworkStatus.Connected(),
|
||||
onLocalAliasChanged = {},
|
||||
customUserProfile = null,
|
||||
@@ -588,9 +459,6 @@ fun PreviewChatInfoLayout() {
|
||||
deleteContact = {},
|
||||
clearChat = {},
|
||||
switchContactAddress = {},
|
||||
abortSwitchContactAddress = {},
|
||||
syncContactConnection = {},
|
||||
syncContactConnectionForce = {},
|
||||
verifyClicked = {},
|
||||
)
|
||||
}
|
||||
@@ -20,8 +20,8 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.*
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.capitalize
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
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.core.content.FileProvider
|
||||
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.call.*
|
||||
@@ -40,7 +41,6 @@ import chat.simplex.app.views.helpers.*
|
||||
import chat.simplex.app.views.helpers.AppBarHeight
|
||||
import com.google.accompanist.insets.ProvideWindowInsets
|
||||
import com.google.accompanist.insets.navigationBarsWithImePadding
|
||||
import chat.simplex.res.MR
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.datetime.Clock
|
||||
@@ -101,7 +101,6 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: () -> Unit) {
|
||||
}
|
||||
}
|
||||
val view = LocalView.current
|
||||
val context = LocalContext.current
|
||||
if (activeChat.value == null || user == null) {
|
||||
chatModel.chatId.value = null
|
||||
} else {
|
||||
@@ -166,8 +165,7 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: () -> Unit) {
|
||||
showMemberInfo = { groupInfo: GroupInfo, member: GroupMember ->
|
||||
hideKeyboard(view)
|
||||
withApi {
|
||||
val r = chatModel.controller.apiGroupMemberInfo(groupInfo.groupId, member.groupMemberId)
|
||||
val stats = r?.second
|
||||
val stats = chatModel.controller.apiGroupMemberInfo(groupInfo.groupId, member.groupMemberId)
|
||||
val (_, code) = if (member.memberActive) {
|
||||
try {
|
||||
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)
|
||||
}
|
||||
},
|
||||
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 ->
|
||||
hideKeyboard(view)
|
||||
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 ->
|
||||
if (searchText.value == value) return@ChatLayout
|
||||
val c = chatModel.getChat(chat.chatInfo.id) ?: return@ChatLayout
|
||||
@@ -385,14 +316,6 @@ fun ChatLayout(
|
||||
startCall: (CallMediaType) -> Unit,
|
||||
acceptCall: (Contact) -> 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,
|
||||
markRead: (CC.ItemRange, unreadCountAfter: Int?) -> Unit,
|
||||
changeNtfsState: (Boolean, currentValue: MutableState<Boolean>) -> Unit,
|
||||
@@ -403,6 +326,7 @@ fun ChatLayout(
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.themedBackground()
|
||||
) {
|
||||
ProvideWindowInsets(windowInsetsAnimationsEnabled = true) {
|
||||
ModalBottomSheetLayout(
|
||||
@@ -428,16 +352,11 @@ fun ChatLayout(
|
||||
modifier = Modifier.navigationBarsWithImePadding(),
|
||||
floatingActionButton = { floatingButton.value() },
|
||||
) { contentPadding ->
|
||||
BoxWithConstraints(Modifier
|
||||
.fillMaxHeight()
|
||||
.padding(contentPadding)
|
||||
) {
|
||||
BoxWithConstraints(Modifier.fillMaxHeight().padding(contentPadding)) {
|
||||
ChatItemsList(
|
||||
chat, unreadCount, composeState, chatItems, searchValue,
|
||||
useLinkPreviews, linkMode, chatModelIncognito, showMemberInfo, loadPrevMessages, deleteMessage,
|
||||
receiveFile, cancelFile, joinGroup, acceptCall, acceptFeature,
|
||||
updateContactStats, updateMemberStats, syncContactConnection, syncMemberConnection, findModelChat, findModelMember,
|
||||
setReaction, showItemDetails, markRead, setFloatingButton, onComposed,
|
||||
receiveFile, cancelFile, joinGroup, acceptCall, acceptFeature, markRead, setFloatingButton, onComposed,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -471,7 +390,7 @@ fun ChatInfoToolbar(
|
||||
val barButtons = arrayListOf<@Composable RowScope.() -> Unit>()
|
||||
val menuItems = arrayListOf<@Composable () -> Unit>()
|
||||
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
|
||||
showSearch = true
|
||||
})
|
||||
@@ -483,11 +402,11 @@ fun ChatInfoToolbar(
|
||||
showMenu.value = false
|
||||
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 {
|
||||
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
|
||||
startCall(CallMediaType.Video)
|
||||
})
|
||||
@@ -498,15 +417,15 @@ fun ChatInfoToolbar(
|
||||
showMenu.value = false
|
||||
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) }
|
||||
menuItems.add {
|
||||
ItemAction(
|
||||
if (ntfsEnabled.value) stringResource(MR.strings.mute_chat) else stringResource(MR.strings.unmute_chat),
|
||||
if (ntfsEnabled.value) painterResource(MR.images.ic_notifications_off) else painterResource(MR.images.ic_notifications),
|
||||
if (ntfsEnabled.value) stringResource(R.string.mute_chat) else stringResource(R.string.unmute_chat),
|
||||
if (ntfsEnabled.value) painterResource(R.drawable.ic_notifications_off) else painterResource(R.drawable.ic_notifications),
|
||||
onClick = {
|
||||
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
|
||||
@@ -520,7 +439,7 @@ fun ChatInfoToolbar(
|
||||
|
||||
barButtons.add {
|
||||
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
|
||||
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)
|
||||
@@ -610,14 +529,6 @@ fun BoxWithConstraintsScope.ChatItemsList(
|
||||
joinGroup: (Long) -> Unit,
|
||||
acceptCall: (Contact) -> 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,
|
||||
setFloatingButton: (@Composable () -> Unit) -> Unit,
|
||||
onComposed: () -> Unit,
|
||||
@@ -705,13 +616,12 @@ 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 (cItem.chatDir is CIDirection.GroupRcv) {
|
||||
val prevItem = if (i < reversedChatItems.lastIndex) reversedChatItems[i + 1] else null
|
||||
val member = cItem.chatDir.groupMember
|
||||
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) {
|
||||
val contactId = member.memberContactId
|
||||
if (contactId == null) {
|
||||
@@ -731,22 +641,22 @@ fun BoxWithConstraintsScope.ChatItemsList(
|
||||
} else {
|
||||
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 {
|
||||
Box(Modifier.padding(start = if (voiceWithTransparentBack) 12.dp else 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)
|
||||
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, scrollToItem = scrollToItem)
|
||||
}
|
||||
}
|
||||
} else { // direct message
|
||||
val sent = cItem.chatDir.sent
|
||||
Box(
|
||||
Modifier.padding(
|
||||
start = if (sent && !voiceWithTransparentBack) 76.dp else 12.dp,
|
||||
end = if (sent || voiceWithTransparentBack) 12.dp else 76.dp,
|
||||
start = if (sent) 76.dp else 12.dp,
|
||||
end = if (sent) 12.dp else 76.dp,
|
||||
).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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -884,8 +794,8 @@ fun BoxWithConstraintsScope.FloatingButtons(
|
||||
|
||||
DefaultDropdownMenu(showDropDown, offset = DpOffset(maxWidth - DEFAULT_PADDING, 24.dp + fabSize)) {
|
||||
ItemAction(
|
||||
generalGetString(MR.strings.mark_read),
|
||||
painterResource(MR.images.ic_check),
|
||||
generalGetString(R.string.mark_read),
|
||||
painterResource(R.drawable.ic_check),
|
||||
onClick = {
|
||||
markRead(
|
||||
CC.ItemRange(minUnreadItemId, chatItems[chatItems.size - listState.layoutInfo.visibleItemsInfo.lastIndex - 1].id - 1),
|
||||
@@ -945,7 +855,6 @@ private fun TopEndFloatingButton(
|
||||
FloatingActionButton(
|
||||
{}, // no action here
|
||||
modifier.size(48.dp),
|
||||
backgroundColor = MaterialTheme.colors.secondaryVariant,
|
||||
elevation = FloatingActionButtonDefaults.elevation(0.dp, 0.dp),
|
||||
interactionSource = interactionSource,
|
||||
) {
|
||||
@@ -972,8 +881,7 @@ private fun bottomEndFloatingButton(
|
||||
FloatingActionButton(
|
||||
onClick = onClickCounter,
|
||||
elevation = FloatingActionButtonDefaults.elevation(0.dp, 0.dp, 0.dp, 0.dp),
|
||||
modifier = Modifier.size(48.dp),
|
||||
backgroundColor = MaterialTheme.colors.secondaryVariant,
|
||||
modifier = Modifier.size(48.dp)
|
||||
) {
|
||||
Text(
|
||||
unreadCountStr(unreadCount),
|
||||
@@ -988,11 +896,10 @@ private fun bottomEndFloatingButton(
|
||||
FloatingActionButton(
|
||||
onClick = onClickArrowDown,
|
||||
elevation = FloatingActionButtonDefaults.elevation(0.dp, 0.dp, 0.dp, 0.dp),
|
||||
modifier = Modifier.size(48.dp),
|
||||
backgroundColor = MaterialTheme.colors.secondaryVariant,
|
||||
modifier = Modifier.size(48.dp)
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(MR.images.ic_keyboard_arrow_down),
|
||||
painter = painterResource(R.drawable.ic_keyboard_arrow_down),
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colors.primary
|
||||
)
|
||||
@@ -1032,7 +939,7 @@ private fun providerForGallery(
|
||||
scrollTo: (Int) -> Unit
|
||||
): ImageGalleryProvider {
|
||||
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>? {
|
||||
var processedInternalIndex = -skipInternalIndex.sign
|
||||
@@ -1059,15 +966,15 @@ private fun providerForGallery(
|
||||
val item = item(internalIndex, initialChatId)?.second ?: return null
|
||||
return when (item.content.msgContent) {
|
||||
is MsgContent.MCImage -> {
|
||||
val imageBitmap: Bitmap? = getLoadedImage(item.file)
|
||||
val filePath = getLoadedFilePath(item.file)
|
||||
val imageBitmap: Bitmap? = getLoadedImage(SimplexApp.context, item.file)
|
||||
val filePath = getLoadedFilePath(SimplexApp.context, item.file)
|
||||
if (imageBitmap != null && filePath != null) {
|
||||
val uri = FileProvider.getUriForFile(SimplexApp.context, "${BuildConfig.APPLICATION_ID}.provider", File(filePath))
|
||||
ProviderMedia.Image(uri, imageBitmap)
|
||||
} else null
|
||||
}
|
||||
is MsgContent.MCVideo -> {
|
||||
val filePath = getLoadedFilePath(item.file)
|
||||
val filePath = getLoadedFilePath(SimplexApp.context, item.file)
|
||||
if (filePath != null) {
|
||||
val uri = FileProvider.getUriForFile(SimplexApp.context, "${BuildConfig.APPLICATION_ID}.provider", File(filePath))
|
||||
ProviderMedia.Video(uri, (item.content.msgContent as MsgContent.MCVideo).image)
|
||||
@@ -1169,14 +1076,6 @@ fun PreviewChatLayout() {
|
||||
startCall = {},
|
||||
acceptCall = { _ -> },
|
||||
acceptFeature = { _, _, _ -> },
|
||||
updateContactStats = { },
|
||||
updateMemberStats = { _, _ -> },
|
||||
syncContactConnection = { },
|
||||
syncMemberConnection = { _, _ -> },
|
||||
findModelChat = { null },
|
||||
findModelMember = { null },
|
||||
setReaction = { _, _, _, _ -> },
|
||||
showItemDetails = { _, _ -> },
|
||||
addMembers = { _ -> },
|
||||
markRead = { _, _ -> },
|
||||
changeNtfsState = { _, _ -> },
|
||||
@@ -1237,14 +1136,6 @@ fun PreviewGroupChatLayout() {
|
||||
startCall = {},
|
||||
acceptCall = { _ -> },
|
||||
acceptFeature = { _, _, _ -> },
|
||||
updateContactStats = { },
|
||||
updateMemberStats = { _, _ -> },
|
||||
syncContactConnection = { },
|
||||
syncMemberConnection = { _, _ -> },
|
||||
findModelChat = { null },
|
||||
findModelMember = { null },
|
||||
setReaction = { _, _, _, _ -> },
|
||||
showItemDetails = { _, _ -> },
|
||||
addMembers = { _ -> },
|
||||
markRead = { _, _ -> },
|
||||
changeNtfsState = { _, _ -> },
|
||||
@@ -5,13 +5,12 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
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.R
|
||||
import chat.simplex.app.ui.theme.*
|
||||
import chat.simplex.res.MR
|
||||
|
||||
@Composable
|
||||
fun ComposeFileView(fileName: String, cancelFile: () -> Unit, cancelEnabled: Boolean) {
|
||||
@@ -25,8 +24,8 @@ fun ComposeFileView(fileName: String, cancelFile: () -> Unit, cancelEnabled: Boo
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_draft_filled),
|
||||
stringResource(MR.strings.icon_descr_file),
|
||||
painterResource(R.drawable.ic_draft_filled),
|
||||
stringResource(R.string.icon_descr_file),
|
||||
Modifier
|
||||
.padding(start = 4.dp, end = 2.dp)
|
||||
.size(36.dp),
|
||||
@@ -37,8 +36,8 @@ fun ComposeFileView(fileName: String, cancelFile: () -> Unit, cancelEnabled: Boo
|
||||
if (cancelEnabled) {
|
||||
IconButton(onClick = cancelFile, modifier = Modifier.padding(0.dp)) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_close),
|
||||
contentDescription = stringResource(MR.strings.icon_descr_cancel_file_preview),
|
||||
painterResource(R.drawable.ic_close),
|
||||
contentDescription = stringResource(R.string.icon_descr_cancel_file_preview),
|
||||
tint = MaterialTheme.colors.primary,
|
||||
modifier = Modifier.padding(10.dp)
|
||||
)
|
||||
@@ -11,14 +11,13 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
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 chat.simplex.app.R
|
||||
import chat.simplex.app.ui.theme.*
|
||||
import chat.simplex.app.views.helpers.UploadContent
|
||||
import chat.simplex.app.views.helpers.base64ToBitmap
|
||||
import chat.simplex.res.MR
|
||||
|
||||
@Composable
|
||||
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)
|
||||
)
|
||||
Icon(
|
||||
painterResource(MR.images.ic_videocam_filled),
|
||||
painterResource(R.drawable.ic_videocam_filled),
|
||||
"preview video",
|
||||
Modifier
|
||||
.size(20.dp),
|
||||
@@ -65,8 +64,8 @@ fun ComposeImageView(media: ComposePreview.MediaPreview, cancelImages: () -> Uni
|
||||
if (cancelEnabled) {
|
||||
IconButton(onClick = cancelImages) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_close),
|
||||
contentDescription = stringResource(MR.strings.icon_descr_cancel_image_preview),
|
||||
painterResource(R.drawable.ic_close),
|
||||
contentDescription = stringResource(R.string.icon_descr_cancel_image_preview),
|
||||
tint = MaterialTheme.colors.primary,
|
||||
)
|
||||
}
|
||||
@@ -16,7 +16,6 @@ import android.webkit.MimeTypeMap
|
||||
import android.widget.Toast
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContract
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.*
|
||||
@@ -27,8 +26,8 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
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.core.content.ContextCompat
|
||||
import chat.simplex.app.*
|
||||
@@ -36,7 +35,6 @@ import chat.simplex.app.R
|
||||
import chat.simplex.app.model.*
|
||||
import chat.simplex.app.views.chat.item.*
|
||||
import chat.simplex.app.views.helpers.*
|
||||
import chat.simplex.res.MR
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.serialization.*
|
||||
@@ -120,7 +118,7 @@ data class ComposeState(
|
||||
|
||||
val attachmentDisabled: Boolean
|
||||
get() {
|
||||
if (editing || liveMessage != null || inProgress) return true
|
||||
if (editing || liveMessage != null) return true
|
||||
return when (preview) {
|
||||
ComposePreview.NoPreview -> false
|
||||
is ComposePreview.CLinkPreview -> false
|
||||
@@ -194,7 +192,7 @@ fun ComposeView(
|
||||
if (isGranted) {
|
||||
cameraLauncher.launchWithFallback()
|
||||
} 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? ->
|
||||
@@ -202,7 +200,7 @@ fun ComposeView(
|
||||
val imagesPreview = ArrayList<String>()
|
||||
uris.forEach { uri ->
|
||||
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 {
|
||||
isImage -> {
|
||||
// Image
|
||||
@@ -210,17 +208,17 @@ fun ComposeView(
|
||||
bitmap = if (drawable != null) getBitmapFromUri(uri) else null
|
||||
val isAnimNewApi = Build.VERSION.SDK_INT >= 28 && drawable is AnimatedImageDrawable
|
||||
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) {
|
||||
// It's a gif or webp
|
||||
val fileSize = getFileSize(uri)
|
||||
val fileSize = getFileSize(context, uri)
|
||||
if (fileSize != null && fileSize <= maxFileSize) {
|
||||
content.add(UploadContent.AnimatedImage(uri))
|
||||
} else {
|
||||
bitmap = null
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(MR.strings.large_file),
|
||||
String.format(generalGetString(MR.strings.maximum_supported_file_size), formatBytes(maxFileSize))
|
||||
generalGetString(R.string.large_file),
|
||||
String.format(generalGetString(R.string.maximum_supported_file_size), formatBytes(maxFileSize))
|
||||
)
|
||||
}
|
||||
} else {
|
||||
@@ -245,16 +243,16 @@ fun ComposeView(
|
||||
}
|
||||
val processPickedFile = { uri: Uri?, text: String? ->
|
||||
if (uri != null) {
|
||||
val fileSize = getFileSize(uri)
|
||||
val fileSize = getFileSize(context, uri)
|
||||
if (fileSize != null && fileSize <= maxFileSize) {
|
||||
val fileName = getFileName(uri)
|
||||
val fileName = getFileName(SimplexApp.context, uri)
|
||||
if (fileName != null) {
|
||||
composeState.value = composeState.value.copy(message = text ?: composeState.value.message, preview = ComposePreview.FilePreview(fileName, uri))
|
||||
}
|
||||
} else {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(MR.strings.large_file),
|
||||
String.format(generalGetString(MR.strings.maximum_supported_file_size), formatBytes(maxFileSize))
|
||||
generalGetString(R.string.large_file),
|
||||
String.format(generalGetString(R.string.maximum_supported_file_size), formatBytes(maxFileSize))
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -368,27 +366,22 @@ fun ComposeView(
|
||||
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(
|
||||
type = cInfo.chatType,
|
||||
id = cInfo.apiId,
|
||||
file = file,
|
||||
quotedItemId = quoted,
|
||||
mc = mc,
|
||||
live = live,
|
||||
ttl = ttl
|
||||
live = live
|
||||
)
|
||||
if (aChatItem != null) {
|
||||
chatModel.addChatItem(cInfo, aChatItem.chatItem)
|
||||
return aChatItem.chatItem
|
||||
}
|
||||
if (file != null) removeFile(file)
|
||||
return null
|
||||
if (aChatItem != null) chatModel.addChatItem(cInfo, aChatItem.chatItem)
|
||||
return aChatItem?.chatItem
|
||||
}
|
||||
|
||||
|
||||
|
||||
suspend fun sendMessageAsync(text: String?, live: Boolean, ttl: Int?): ChatItem? {
|
||||
suspend fun sendMessageAsync(text: String?, live: Boolean): ChatItem? {
|
||||
val cInfo = chat.chatInfo
|
||||
val cs = composeState.value
|
||||
var sent: ChatItem?
|
||||
@@ -461,9 +454,9 @@ fun ComposeView(
|
||||
is ComposePreview.MediaPreview -> {
|
||||
preview.content.forEachIndexed { index, it ->
|
||||
val file = when (it) {
|
||||
is UploadContent.SimpleImage -> saveImage(it.uri)
|
||||
is UploadContent.AnimatedImage -> saveAnimImage(it.uri)
|
||||
is UploadContent.Video -> saveFileFromUri(it.uri)
|
||||
is UploadContent.SimpleImage -> saveImage(context, it.uri)
|
||||
is UploadContent.AnimatedImage -> saveAnimImage(context, it.uri)
|
||||
is UploadContent.Video -> saveFileFromUri(context, it.uri)
|
||||
}
|
||||
if (file != null) {
|
||||
files.add(file)
|
||||
@@ -478,7 +471,7 @@ fun ComposeView(
|
||||
is ComposePreview.VoicePreview -> {
|
||||
val tmpFile = File(preview.voice)
|
||||
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) {
|
||||
Files.move(tmpFile.toPath(), actualFile.toPath())
|
||||
}
|
||||
@@ -487,7 +480,7 @@ fun ComposeView(
|
||||
msgs.add(MsgContent.MCVoice(if (msgs.isEmpty()) msgText else "", preview.durationMs / 1000))
|
||||
}
|
||||
is ComposePreview.FilePreview -> {
|
||||
val file = saveFileFromUri(preview.uri)
|
||||
val file = saveFileFromUri(context, preview.uri)
|
||||
if (file != null) {
|
||||
files.add((file))
|
||||
msgs.add(MsgContent.MCFile(if (msgs.isEmpty()) msgText else ""))
|
||||
@@ -502,8 +495,7 @@ fun ComposeView(
|
||||
msgs.forEachIndexed { index, content ->
|
||||
if (index > 0) delay(100)
|
||||
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,
|
||||
ttl = ttl
|
||||
if (content !is MsgContent.MCVoice && index == msgs.lastIndex) live else false
|
||||
)
|
||||
}
|
||||
if (sent == null &&
|
||||
@@ -511,16 +503,16 @@ fun ComposeView(
|
||||
cs.preview is ComposePreview.FilePreview ||
|
||||
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)
|
||||
return sent
|
||||
}
|
||||
|
||||
fun sendMessage(ttl: Int?) {
|
||||
fun sendMessage() {
|
||||
withBGApi {
|
||||
sendMessageAsync(null, false, ttl)
|
||||
sendMessageAsync(null, false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -595,8 +587,8 @@ fun ComposeView(
|
||||
suspend fun sendLiveMessage() {
|
||||
val cs = composeState.value
|
||||
val typedMsg = cs.message
|
||||
if ((cs.sendEnabled() || cs.contextItem is ComposeContextItem.QuotedItem) && (cs.liveMessage == null || !cs.liveMessage.sent)) {
|
||||
val ci = sendMessageAsync(typedMsg, live = true, ttl = null)
|
||||
if ((cs.sendEnabled() || cs.contextItem is ComposeContextItem.QuotedItem) && (cs.liveMessage == null || !cs.liveMessage?.sent)) {
|
||||
val ci = sendMessageAsync(typedMsg, live = true)
|
||||
if (ci != null) {
|
||||
composeState.value = composeState.value.copy(liveMessage = LiveMessage(ci, typedMsg = typedMsg, sentMsg = typedMsg, sent = true))
|
||||
}
|
||||
@@ -617,7 +609,7 @@ fun ComposeView(
|
||||
if (liveMessage != null) {
|
||||
val sentMsg = liveMessageToSend(liveMessage, typedMsg)
|
||||
if (sentMsg != null) {
|
||||
val ci = sendMessageAsync(sentMsg, live = true, ttl = null)
|
||||
val ci = sendMessageAsync(sentMsg, live = true)
|
||||
if (ci != null) {
|
||||
composeState.value = composeState.value.copy(liveMessage = LiveMessage(ci, typedMsg = typedMsg, sentMsg = sentMsg, sent = true))
|
||||
}
|
||||
@@ -631,27 +623,23 @@ fun ComposeView(
|
||||
fun previewView() {
|
||||
when (val preview = composeState.value.preview) {
|
||||
ComposePreview.NoPreview -> {}
|
||||
is ComposePreview.CLinkPreview -> ComposeLinkView(
|
||||
preview.linkPreview,
|
||||
::cancelLinkPreview,
|
||||
cancelEnabled = !composeState.value.inProgress
|
||||
)
|
||||
is ComposePreview.CLinkPreview -> ComposeLinkView(preview.linkPreview, ::cancelLinkPreview)
|
||||
is ComposePreview.MediaPreview -> ComposeImageView(
|
||||
preview,
|
||||
::cancelImages,
|
||||
cancelEnabled = !composeState.value.editing && !composeState.value.inProgress
|
||||
cancelEnabled = !composeState.value.editing
|
||||
)
|
||||
is ComposePreview.VoicePreview -> ComposeVoiceView(
|
||||
preview.voice,
|
||||
preview.durationMs,
|
||||
preview.finished,
|
||||
cancelEnabled = !composeState.value.editing && !composeState.value.inProgress,
|
||||
cancelEnabled = !composeState.value.editing,
|
||||
::cancelVoice
|
||||
)
|
||||
is ComposePreview.FilePreview -> ComposeFileView(
|
||||
preview.fileName,
|
||||
::cancelFile,
|
||||
cancelEnabled = !composeState.value.editing && !composeState.value.inProgress
|
||||
cancelEnabled = !composeState.value.editing
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -660,23 +648,15 @@ fun ComposeView(
|
||||
fun contextItemView() {
|
||||
when (val contextItem = composeState.value.contextItem) {
|
||||
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)
|
||||
}
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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) {
|
||||
// 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
|
||||
@@ -694,43 +674,21 @@ fun ComposeView(
|
||||
val userIsObserver = rememberUpdatedState(chat.userIsObserver)
|
||||
|
||||
Column {
|
||||
if (composeState.value.preview !is ComposePreview.VoicePreview || composeState.value.editing) {
|
||||
contextItemView()
|
||||
when {
|
||||
composeState.value.editing && composeState.value.preview is ComposePreview.VoicePreview -> {}
|
||||
composeState.value.editing && composeState.value.preview is ComposePreview.FilePreview -> {}
|
||||
else -> previewView()
|
||||
}
|
||||
} else {
|
||||
Box {
|
||||
Box(Modifier.align(Alignment.TopStart).padding(bottom = 69.dp)) {
|
||||
contextItemView()
|
||||
}
|
||||
Box(Modifier.align(Alignment.BottomStart)) {
|
||||
previewView()
|
||||
}
|
||||
}
|
||||
contextItemView()
|
||||
when {
|
||||
composeState.value.editing && composeState.value.preview is ComposePreview.VoicePreview -> {}
|
||||
composeState.value.editing && composeState.value.preview is ComposePreview.FilePreview -> {}
|
||||
else -> previewView()
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier.padding(end = 8.dp),
|
||||
verticalAlignment = Alignment.Bottom,
|
||||
) {
|
||||
val isGroupAndProhibitedFiles = chat.chatInfo is ChatInfo.Group && !chat.chatInfo.groupInfo.fullGroupPreferences.files.on
|
||||
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) {
|
||||
IconButton(showChooseAttachment, enabled = !composeState.value.attachmentDisabled && rememberUpdatedState(chat.userCanSend).value) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_attach_file_filled_500),
|
||||
contentDescription = stringResource(MR.strings.attach),
|
||||
tint = if (!composeState.value.attachmentDisabled && userCanSend.value && !isGroupAndProhibitedFiles) MaterialTheme.colors.primary else MaterialTheme.colors.secondary,
|
||||
painterResource(R.drawable.ic_attach_file_filled_500),
|
||||
contentDescription = stringResource(R.string.attach),
|
||||
tint = if (!composeState.value.attachmentDisabled && userCanSend.value) MaterialTheme.colors.primary else MaterialTheme.colors.secondary,
|
||||
modifier = Modifier
|
||||
.size(28.dp)
|
||||
.clip(CircleShape)
|
||||
@@ -782,12 +740,10 @@ fun ComposeView(
|
||||
if (orientation == activity.resources.configuration.orientation) {
|
||||
val cs = composeState.value
|
||||
if (cs.liveMessage != null && (cs.message.isNotEmpty() || cs.liveMessage.sent)) {
|
||||
sendMessage(null)
|
||||
sendMessage()
|
||||
resetLinkPreview()
|
||||
clearCurrentDraft()
|
||||
deleteUnusedFiles()
|
||||
} else if (composeState.value.inProgress) {
|
||||
clearCurrentDraft()
|
||||
} else if (!composeState.value.empty) {
|
||||
if (cs.preview is ComposePreview.VoicePreview && !cs.preview.finished) {
|
||||
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(
|
||||
composeState,
|
||||
showVoiceRecordIcon = true,
|
||||
@@ -815,10 +770,8 @@ fun ComposeView(
|
||||
allowVoiceToContact = ::allowVoiceToContact,
|
||||
userIsObserver = userIsObserver.value,
|
||||
userCanSend = userCanSend.value,
|
||||
timedMessageAllowed = timedMessageAllowed,
|
||||
customDisappearingMessageTimePref = chatModel.controller.appPrefs.customDisappearingMessageTime,
|
||||
sendMessage = { ttl ->
|
||||
sendMessage(ttl)
|
||||
sendMessage = {
|
||||
sendMessage()
|
||||
resetLinkPreview()
|
||||
},
|
||||
sendLiveMessage = ::sendLiveMessage,
|
||||
@@ -861,7 +814,7 @@ class PickMultipleImagesFromGallery: ActivityResultContract<Int, List<Uri>>() {
|
||||
if (uri != null) uris.add(uri)
|
||||
}
|
||||
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
|
||||
}
|
||||
@@ -888,7 +841,7 @@ class PickMultipleVideosFromGallery: ActivityResultContract<Int, List<Uri>>() {
|
||||
if (uri != null) uris.add(uri)
|
||||
}
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
import androidx.compose.animation.core.Animatable
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.model.durationText
|
||||
import chat.simplex.app.ui.theme.*
|
||||
import chat.simplex.app.views.helpers.*
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
|
||||
@Composable
|
||||
fun ComposeVoiceView(
|
||||
filePath: String,
|
||||
recordedDurationMs: Int,
|
||||
finishedRecording: Boolean,
|
||||
cancelEnabled: Boolean,
|
||||
cancelVoice: () -> Unit
|
||||
) {
|
||||
BoxWithConstraints(Modifier
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
val audioPlaying = rememberSaveable { mutableStateOf(false) }
|
||||
val progress = rememberSaveable { mutableStateOf(0) }
|
||||
val duration = rememberSaveable(recordedDurationMs) { mutableStateOf(recordedDurationMs) }
|
||||
val progressBarWidth = remember { Animatable(0f) }
|
||||
LaunchedEffect(recordedDurationMs, finishedRecording) {
|
||||
snapshotFlow { progress.value }
|
||||
.distinctUntilChanged()
|
||||
.collect {
|
||||
val startTime = when {
|
||||
finishedRecording -> progress.value
|
||||
else -> recordedDurationMs
|
||||
}
|
||||
val endTime = when {
|
||||
finishedRecording -> duration.value
|
||||
audioPlaying.value -> recordedDurationMs
|
||||
else -> MAX_VOICE_MILLIS_FOR_SENDING
|
||||
}
|
||||
val to = ((startTime.toDouble() / endTime) * maxWidth.value).dp
|
||||
progressBarWidth.animateTo(to.value, audioProgressBarAnimationSpec())
|
||||
}
|
||||
}
|
||||
Spacer(
|
||||
Modifier
|
||||
.requiredWidth(progressBarWidth.value.dp)
|
||||
.padding(top = 58.dp)
|
||||
.height(3.dp)
|
||||
.background(MaterialTheme.colors.primary)
|
||||
)
|
||||
val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage
|
||||
Row(
|
||||
Modifier
|
||||
.height(60.dp)
|
||||
.fillMaxWidth()
|
||||
.padding(top = 8.dp)
|
||||
.background(sentColor),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
IconButton(
|
||||
onClick = {
|
||||
if (!audioPlaying.value) {
|
||||
AudioPlayer.play(filePath, audioPlaying, progress, duration, false)
|
||||
} else {
|
||||
AudioPlayer.pause(audioPlaying, progress)
|
||||
}
|
||||
},
|
||||
enabled = finishedRecording) {
|
||||
Icon(
|
||||
if (audioPlaying.value) painterResource(R.drawable.ic_pause_filled) else painterResource(R.drawable.ic_play_arrow_filled),
|
||||
stringResource(R.string.icon_descr_file),
|
||||
Modifier
|
||||
.padding(start = 4.dp, end = 2.dp)
|
||||
.size(36.dp),
|
||||
tint = if (finishedRecording) MaterialTheme.colors.primary else MaterialTheme.colors.secondary
|
||||
)
|
||||
}
|
||||
val numberInText = remember(recordedDurationMs, progress.value) {
|
||||
derivedStateOf {
|
||||
when {
|
||||
finishedRecording && progress.value == 0 && !audioPlaying.value -> duration.value / 1000
|
||||
finishedRecording -> progress.value / 1000
|
||||
else -> recordedDurationMs / 1000
|
||||
}
|
||||
}
|
||||
}
|
||||
Text(
|
||||
durationText(numberInText.value),
|
||||
fontSize = 18.sp,
|
||||
color = MaterialTheme.colors.secondary,
|
||||
)
|
||||
Spacer(Modifier.weight(1f))
|
||||
if (cancelEnabled) {
|
||||
IconButton(
|
||||
onClick = {
|
||||
AudioPlayer.stop(filePath)
|
||||
cancelVoice()
|
||||
},
|
||||
modifier = Modifier.padding(0.dp)
|
||||
) {
|
||||
Icon(
|
||||
painterResource(R.drawable.ic_close),
|
||||
contentDescription = stringResource(R.string.icon_descr_cancel_file_preview),
|
||||
tint = MaterialTheme.colors.primary,
|
||||
modifier = Modifier.padding(10.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun PreviewComposeAudioView() {
|
||||
SimpleXTheme {
|
||||
ComposeFileView(
|
||||
"test.txt",
|
||||
cancelFile = {},
|
||||
cancelEnabled = true
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -14,13 +14,12 @@ import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Modifier
|
||||
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.model.*
|
||||
import chat.simplex.app.ui.theme.*
|
||||
import chat.simplex.app.views.helpers.*
|
||||
import chat.simplex.app.views.usersettings.PreferenceToggle
|
||||
import chat.simplex.res.MR
|
||||
|
||||
@Composable
|
||||
fun ContactPreferencesView(
|
||||
@@ -82,7 +81,7 @@ private fun ContactPreferencesLayout(
|
||||
.fillMaxWidth()
|
||||
.verticalScroll(rememberScrollState()),
|
||||
) {
|
||||
AppBarTitle(stringResource(MR.strings.contact_preferences))
|
||||
AppBarTitle(stringResource(R.string.contact_preferences))
|
||||
val timedMessages: MutableState<Boolean> = remember(featuresAllowed) { mutableStateOf(featuresAllowed.timedMessagesAllowed) }
|
||||
val onTTLUpdated = { ttl: Int? ->
|
||||
applyPrefs(featuresAllowed.copy(timedMessagesTTL = ttl))
|
||||
@@ -96,11 +95,6 @@ private fun ContactPreferencesLayout(
|
||||
applyPrefs(featuresAllowed.copy(fullDelete = it))
|
||||
}
|
||||
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) }
|
||||
FeatureSection(ChatFeature.Voice, user.fullPreferences.voice.allow, contact.mergedPreferences.voice, allowVoice) {
|
||||
applyPrefs(featuresAllowed.copy(voice = it))
|
||||
@@ -141,18 +135,19 @@ private fun FeatureSection(
|
||||
leadingIcon = true,
|
||||
) {
|
||||
ExposedDropDownSettingRow(
|
||||
generalGetString(MR.strings.chat_preferences_you_allow),
|
||||
generalGetString(R.string.chat_preferences_you_allow),
|
||||
ContactFeatureAllowed.values(userDefault).map { it to it.text },
|
||||
allowFeature,
|
||||
icon = null,
|
||||
enabled = remember { mutableStateOf(feature != ChatFeature.Calls) },
|
||||
onSelected = onSelected
|
||||
)
|
||||
InfoRow(
|
||||
generalGetString(MR.strings.chat_preferences_contact_allows),
|
||||
generalGetString(R.string.chat_preferences_contact_allows),
|
||||
pref.contactPreference.allow.text
|
||||
)
|
||||
}
|
||||
SectionTextFooter(feature.enabledDescription(enabled))
|
||||
SectionTextFooter(feature.enabledDescription(enabled) + (if (feature == ChatFeature.Calls) generalGetString(R.string.available_in_v51) else ""))
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -176,28 +171,20 @@ private fun TimedMessagesFeatureSection(
|
||||
leadingIcon = true,
|
||||
) {
|
||||
PreferenceToggle(
|
||||
generalGetString(MR.strings.chat_preferences_you_allow),
|
||||
generalGetString(R.string.chat_preferences_you_allow),
|
||||
checked = allowFeature.value,
|
||||
) { allow ->
|
||||
onSelected(allow, if (allow) featuresAllowed.timedMessagesTTL ?: 86400 else null)
|
||||
}
|
||||
InfoRow(
|
||||
generalGetString(MR.strings.chat_preferences_contact_allows),
|
||||
generalGetString(R.string.chat_preferences_contact_allows),
|
||||
pref.contactPreference.allow.text
|
||||
)
|
||||
if (featuresAllowed.timedMessagesAllowed) {
|
||||
val ttl = rememberSaveable(featuresAllowed.timedMessagesTTL) { mutableStateOf(featuresAllowed.timedMessagesTTL) }
|
||||
DropdownCustomTimePickerSettingRow(
|
||||
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
|
||||
)
|
||||
TimedMessagesTTLPicker(ttl, onTTLUpdated)
|
||||
} 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))
|
||||
@@ -207,19 +194,31 @@ private fun TimedMessagesFeatureSection(
|
||||
private fun ResetSaveButtons(reset: () -> Unit, save: () -> Unit, disabled: Boolean) {
|
||||
SectionView {
|
||||
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) {
|
||||
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) {
|
||||
AlertManager.shared.showAlertDialogStacked(
|
||||
title = generalGetString(MR.strings.save_preferences_question),
|
||||
confirmText = generalGetString(MR.strings.save_and_notify_contact),
|
||||
dismissText = generalGetString(MR.strings.exit_without_saving),
|
||||
title = generalGetString(R.string.save_preferences_question),
|
||||
confirmText = generalGetString(R.string.save_and_notify_contact),
|
||||
dismissText = generalGetString(R.string.exit_without_saving),
|
||||
onConfirm = save,
|
||||
onDismiss = revert,
|
||||
)
|
||||
@@ -8,15 +8,14 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
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.R
|
||||
import chat.simplex.app.model.*
|
||||
import chat.simplex.app.ui.theme.*
|
||||
import chat.simplex.app.views.chat.item.*
|
||||
import chat.simplex.res.MR
|
||||
import kotlinx.datetime.Clock
|
||||
|
||||
@Composable
|
||||
@@ -47,7 +46,7 @@ fun ContextItemView(
|
||||
.padding(horizontal = 8.dp)
|
||||
.height(20.dp)
|
||||
.width(20.dp),
|
||||
contentDescription = stringResource(MR.strings.icon_descr_context),
|
||||
contentDescription = stringResource(R.string.icon_descr_context),
|
||||
tint = MaterialTheme.colors.secondary,
|
||||
)
|
||||
MarkdownText(
|
||||
@@ -59,8 +58,8 @@ fun ContextItemView(
|
||||
}
|
||||
IconButton(onClick = cancelContextItem) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_close),
|
||||
contentDescription = stringResource(MR.strings.cancel_verb),
|
||||
painterResource(R.drawable.ic_close),
|
||||
contentDescription = stringResource(R.string.cancel_verb),
|
||||
tint = MaterialTheme.colors.primary,
|
||||
modifier = Modifier.padding(10.dp)
|
||||
)
|
||||
@@ -74,7 +73,7 @@ fun PreviewContextItemView() {
|
||||
SimpleXTheme {
|
||||
ContextItemView(
|
||||
contextItem = ChatItem.getSampleData(1, CIDirection.DirectRcv(), Clock.System.now(), "hello"),
|
||||
contextIcon = painterResource(MR.images.ic_edit_filled)
|
||||
contextIcon = painterResource(R.drawable.ic_edit_filled)
|
||||
) {}
|
||||
}
|
||||
}
|
||||
@@ -6,13 +6,12 @@ import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
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.ui.theme.DEFAULT_PADDING
|
||||
import chat.simplex.app.views.helpers.*
|
||||
import chat.simplex.app.views.newchat.QRCodeScanner
|
||||
import com.google.accompanist.permissions.rememberPermissionState
|
||||
import chat.simplex.res.MR
|
||||
|
||||
@Composable
|
||||
fun ScanCodeView(verifyCode: (String?, cb: (Boolean) -> Unit) -> Unit, close: () -> Unit) {
|
||||
@@ -30,7 +29,7 @@ private fun ScanCodeLayout(verifyCode: (String?, cb: (Boolean) -> Unit) -> Unit,
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = DEFAULT_PADDING)
|
||||
) {
|
||||
AppBarTitle(stringResource(MR.strings.scan_code), false)
|
||||
AppBarTitle(stringResource(R.string.scan_code), false)
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
@@ -43,12 +42,12 @@ private fun ScanCodeLayout(verifyCode: (String?, cb: (Boolean) -> Unit) -> Unit,
|
||||
close()
|
||||
} else {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(MR.strings.incorrect_code)
|
||||
title = generalGetString(R.string.incorrect_code)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Text(stringResource(MR.strings.scan_code_from_contacts_app))
|
||||
Text(stringResource(R.string.scan_code_from_contacts_app))
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,7 @@ import androidx.compose.animation.core.*
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
import androidx.compose.runtime.*
|
||||
@@ -29,14 +29,14 @@ import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.*
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
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.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.unit.*
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.core.graphics.drawable.DrawableCompat
|
||||
import androidx.core.view.inputmethod.EditorInfoCompat
|
||||
import androidx.core.view.inputmethod.InputConnectionCompat
|
||||
@@ -44,14 +44,11 @@ import androidx.core.widget.*
|
||||
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.ui.theme.CurrentColors
|
||||
import chat.simplex.app.ui.theme.SimpleXTheme
|
||||
import chat.simplex.app.views.chat.item.ItemAction
|
||||
import chat.simplex.app.views.helpers.*
|
||||
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 java.lang.reflect.Field
|
||||
|
||||
@@ -67,25 +64,13 @@ fun SendMsgView(
|
||||
userIsObserver: Boolean,
|
||||
userCanSend: Boolean,
|
||||
allowVoiceToContact: () -> Unit,
|
||||
timedMessageAllowed: Boolean = false,
|
||||
customDisappearingMessageTimePref: SharedPreference<Int>? = null,
|
||||
sendMessage: (Int?) -> Unit,
|
||||
sendMessage: () -> Unit,
|
||||
sendLiveMessage: (suspend () -> Unit)? = null,
|
||||
updateLiveMessage: (suspend () -> Unit)? = null,
|
||||
cancelLiveMessage: (() -> Unit)? = null,
|
||||
onMessageChange: (String) -> Unit,
|
||||
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)) {
|
||||
val cs = composeState.value
|
||||
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) }
|
||||
NativeKeyboard(composeState, textStyle, showDeleteTextButton, userIsObserver, onMessageChange)
|
||||
// Disable clicks on text field
|
||||
if (cs.preview is ComposePreview.VoicePreview || !userCanSend || cs.inProgress) {
|
||||
Box(
|
||||
Modifier
|
||||
.matchParentSize()
|
||||
.clickable(enabled = !userCanSend, indication = null, interactionSource = remember { MutableInteractionSource() }, onClick = {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(MR.strings.observer_cant_send_message_title),
|
||||
text = generalGetString(MR.strings.observer_cant_send_message_desc)
|
||||
)
|
||||
})
|
||||
if (cs.preview is ComposePreview.VoicePreview || !userCanSend) {
|
||||
Box(Modifier
|
||||
.matchParentSize()
|
||||
.clickable(enabled = !userCanSend, indication = null, interactionSource = remember { MutableInteractionSource() }, onClick = {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(R.string.observer_cant_send_message_title),
|
||||
text = generalGetString(R.string.observer_cant_send_message_desc)
|
||||
)
|
||||
})
|
||||
)
|
||||
}
|
||||
if (showDeleteTextButton.value) {
|
||||
@@ -140,11 +124,10 @@ fun SendMsgView(
|
||||
else ->
|
||||
RecordVoiceView(recState, stopRecOnNextClick)
|
||||
}
|
||||
if (sendLiveMessage != null
|
||||
&& updateLiveMessage != null
|
||||
&& (cs.preview !is ComposePreview.VoicePreview || !stopRecOnNextClick.value)
|
||||
&& cs.contextItem is ComposeContextItem.NoContextItem
|
||||
) {
|
||||
if (sendLiveMessage != null
|
||||
&& updateLiveMessage != null
|
||||
&& (cs.preview !is ComposePreview.VoicePreview || !stopRecOnNextClick.value)
|
||||
&& cs.contextItem is ComposeContextItem.NoContextItem) {
|
||||
Spacer(Modifier.width(10.dp))
|
||||
StartLiveMessageButton(userCanSend) {
|
||||
if (composeState.value.preview is ComposePreview.NoPreview) {
|
||||
@@ -161,55 +144,29 @@ fun SendMsgView(
|
||||
}
|
||||
else -> {
|
||||
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() ||
|
||||
(!allowedVoiceByPrefs && cs.preview is ComposePreview.VoicePreview) ||
|
||||
cs.endLiveDisabled
|
||||
val showDropdown = rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
@Composable
|
||||
fun MenuItems(): List<@Composable () -> Unit> {
|
||||
val menuItems = mutableListOf<@Composable () -> Unit>()
|
||||
|
||||
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()) {
|
||||
(!allowedVoiceByPrefs && cs.preview is ComposePreview.VoicePreview) ||
|
||||
cs.endLiveDisabled
|
||||
if (cs.liveMessage == null &&
|
||||
cs.preview !is ComposePreview.VoicePreview && !cs.editing &&
|
||||
cs.contextItem is ComposeContextItem.NoContextItem &&
|
||||
sendLiveMessage != null && updateLiveMessage != null
|
||||
) {
|
||||
val showDropdown = rememberSaveable { mutableStateOf(false) }
|
||||
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 {
|
||||
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
|
||||
private fun NativeKeyboard(
|
||||
composeState: MutableState<ComposeState>,
|
||||
@@ -386,13 +250,7 @@ private fun NativeKeyboard(
|
||||
Log.e(chat.simplex.app.TAG, e.stackTraceToString())
|
||||
}
|
||||
}
|
||||
editText.doOnTextChanged { text, _, _, _ ->
|
||||
if (!composeState.value.inProgress) {
|
||||
onMessageChange(text.toString())
|
||||
} else if (text.toString() != composeState.value.message) {
|
||||
editText.setText(composeState.value.message)
|
||||
}
|
||||
}
|
||||
editText.doOnTextChanged { text, _, _, _ -> onMessageChange(text.toString()) }
|
||||
editText.doAfterTextChanged { text -> if (composeState.value.preview is ComposePreview.VoicePreview && text.toString() != "") editText.setText("") }
|
||||
editText
|
||||
}) {
|
||||
@@ -412,17 +270,17 @@ private fun NativeKeyboard(
|
||||
imm.showSoftInput(it, InputMethodManager.SHOW_IMPLICIT)
|
||||
showKeyboard = false
|
||||
}
|
||||
showDeleteTextButton.value = it.lineCount >= 4 && !cs.inProgress
|
||||
showDeleteTextButton.value = it.lineCount >= 4
|
||||
}
|
||||
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) {
|
||||
ComposeOverlay(MR.strings.you_are_observer, textStyle, padding)
|
||||
ComposeOverlay(R.string.you_are_observer, textStyle, padding)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ComposeOverlay(textId: StringResource, textStyle: MutableState<TextStyle>, padding: PaddingValues) {
|
||||
private fun ComposeOverlay(textId: Int, textStyle: MutableState<TextStyle>, padding: PaddingValues) {
|
||||
Text(
|
||||
generalGetString(textId),
|
||||
Modifier.padding(padding),
|
||||
@@ -437,13 +295,13 @@ private fun BoxScope.DeleteTextButton(composeState: MutableState<ComposeState>)
|
||||
{ composeState.value = composeState.value.copy(message = "") },
|
||||
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
|
||||
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() } }
|
||||
val stopRecordingAndAddAudio: () -> Unit = {
|
||||
recState.value.filePathNullable?.let {
|
||||
@@ -494,8 +352,8 @@ private fun RecordVoiceView(recState: MutableState<RecordingState>, stopRecOnNex
|
||||
private fun DisallowedVoiceButton(enabled: Boolean, onClick: () -> Unit) {
|
||||
IconButton(onClick, Modifier.size(36.dp), enabled = enabled) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_keyboard_voice),
|
||||
stringResource(MR.strings.icon_descr_record_voice_message),
|
||||
painterResource(R.drawable.ic_keyboard_voice),
|
||||
stringResource(R.string.icon_descr_record_voice_message),
|
||||
tint = MaterialTheme.colors.secondary,
|
||||
modifier = Modifier
|
||||
.size(36.dp)
|
||||
@@ -508,8 +366,8 @@ private fun DisallowedVoiceButton(enabled: Boolean, onClick: () -> Unit) {
|
||||
private fun VoiceButtonWithoutPermission(onClick: () -> Unit) {
|
||||
IconButton(onClick, Modifier.size(36.dp)) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_keyboard_voice_filled),
|
||||
stringResource(MR.strings.icon_descr_record_voice_message),
|
||||
painterResource(R.drawable.ic_keyboard_voice_filled),
|
||||
stringResource(R.string.icon_descr_record_voice_message),
|
||||
tint = MaterialTheme.colors.primary,
|
||||
modifier = Modifier
|
||||
.size(34.dp)
|
||||
@@ -540,8 +398,8 @@ private fun LockToCurrentOrientationUntilDispose() {
|
||||
private fun StopRecordButton(onClick: () -> Unit) {
|
||||
IconButton(onClick, Modifier.size(36.dp)) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_stop_filled),
|
||||
stringResource(MR.strings.icon_descr_record_voice_message),
|
||||
painterResource(R.drawable.ic_stop_filled),
|
||||
stringResource(R.string.icon_descr_record_voice_message),
|
||||
tint = MaterialTheme.colors.primary,
|
||||
modifier = Modifier
|
||||
.size(36.dp)
|
||||
@@ -554,8 +412,8 @@ private fun StopRecordButton(onClick: () -> Unit) {
|
||||
private fun RecordVoiceButton(interactionSource: MutableInteractionSource) {
|
||||
IconButton({}, Modifier.size(36.dp), interactionSource = interactionSource) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_keyboard_voice_filled),
|
||||
stringResource(MR.strings.icon_descr_record_voice_message),
|
||||
painterResource(R.drawable.ic_keyboard_voice_filled),
|
||||
stringResource(R.string.icon_descr_record_voice_message),
|
||||
tint = MaterialTheme.colors.primary,
|
||||
modifier = Modifier
|
||||
.size(34.dp)
|
||||
@@ -575,8 +433,8 @@ private fun CancelLiveMessageButton(
|
||||
) {
|
||||
IconButton(onClick, Modifier.size(36.dp)) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_close),
|
||||
stringResource(MR.strings.icon_descr_cancel_live_message),
|
||||
painterResource(R.drawable.ic_close),
|
||||
stringResource(R.string.icon_descr_cancel_live_message),
|
||||
tint = MaterialTheme.colors.primary,
|
||||
modifier = Modifier
|
||||
.size(36.dp)
|
||||
@@ -591,14 +449,14 @@ private fun SendMsgButton(
|
||||
sizeDp: Animatable<Float, AnimationVector1D>,
|
||||
alpha: Animatable<Float, AnimationVector1D>,
|
||||
enabled: Boolean,
|
||||
sendMessage: (Int?) -> Unit,
|
||||
sendMessage: () -> Unit,
|
||||
onLongClick: (() -> Unit)? = null
|
||||
) {
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
Box(
|
||||
modifier = Modifier.requiredSize(36.dp)
|
||||
.combinedClickable(
|
||||
onClick = { sendMessage(null) },
|
||||
onClick = sendMessage,
|
||||
onLongClick = onLongClick,
|
||||
enabled = enabled,
|
||||
role = Role.Button,
|
||||
@@ -609,7 +467,7 @@ private fun SendMsgButton(
|
||||
) {
|
||||
Icon(
|
||||
icon,
|
||||
stringResource(MR.strings.icon_descr_send_message),
|
||||
stringResource(R.string.icon_descr_send_message),
|
||||
tint = Color.White,
|
||||
modifier = Modifier
|
||||
.size(sizeDp.value.dp)
|
||||
@@ -638,7 +496,7 @@ private fun StartLiveMessageButton(enabled: Boolean, onClick: () -> Unit) {
|
||||
) {
|
||||
Icon(
|
||||
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,
|
||||
modifier = Modifier
|
||||
.size(36.dp)
|
||||
@@ -689,9 +547,9 @@ private fun startLiveMessage(
|
||||
start()
|
||||
} else {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(MR.strings.live_message),
|
||||
text = generalGetString(MR.strings.send_live_message_desc),
|
||||
confirmText = generalGetString(MR.strings.send_verb),
|
||||
title = generalGetString(R.string.live_message),
|
||||
text = generalGetString(R.string.send_live_message_desc),
|
||||
confirmText = generalGetString(R.string.send_verb),
|
||||
onConfirm = {
|
||||
liveMessageAlertShown.set(true)
|
||||
start()
|
||||
@@ -701,22 +559,22 @@ private fun startLiveMessage(
|
||||
|
||||
private fun showNeedToAllowVoiceAlert(onConfirm: () -> Unit) {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(MR.strings.allow_voice_messages_question),
|
||||
text = generalGetString(MR.strings.you_need_to_allow_to_send_voice),
|
||||
confirmText = generalGetString(MR.strings.allow_verb),
|
||||
dismissText = generalGetString(MR.strings.cancel_verb),
|
||||
title = generalGetString(R.string.allow_voice_messages_question),
|
||||
text = generalGetString(R.string.you_need_to_allow_to_send_voice),
|
||||
confirmText = generalGetString(R.string.allow_verb),
|
||||
dismissText = generalGetString(R.string.cancel_verb),
|
||||
onConfirm = onConfirm,
|
||||
)
|
||||
}
|
||||
|
||||
private fun showDisabledVoiceAlert(isDirectChat: Boolean) {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(MR.strings.voice_messages_prohibited),
|
||||
title = generalGetString(R.string.voice_messages_prohibited),
|
||||
text = generalGetString(
|
||||
if (isDirectChat)
|
||||
MR.strings.ask_your_contact_to_enable_voice
|
||||
R.string.ask_your_contact_to_enable_voice
|
||||
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,
|
||||
userCanSend = true,
|
||||
allowVoiceToContact = {},
|
||||
timedMessageAllowed = false,
|
||||
sendMessage = {},
|
||||
onMessageChange = { _ -> },
|
||||
textStyle = textStyle
|
||||
@@ -774,7 +631,6 @@ fun PreviewSendMsgViewEditing() {
|
||||
userIsObserver = false,
|
||||
userCanSend = true,
|
||||
allowVoiceToContact = {},
|
||||
timedMessageAllowed = false,
|
||||
sendMessage = {},
|
||||
onMessageChange = { _ -> },
|
||||
textStyle = textStyle
|
||||
@@ -805,7 +661,6 @@ fun PreviewSendMsgViewInProgress() {
|
||||
userIsObserver = false,
|
||||
userCanSend = true,
|
||||
allowVoiceToContact = {},
|
||||
timedMessageAllowed = false,
|
||||
sendMessage = {},
|
||||
onMessageChange = { _ -> },
|
||||
textStyle = textStyle
|
||||
@@ -10,8 +10,9 @@ import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.unit.dp
|
||||
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.views.helpers.*
|
||||
import chat.simplex.app.views.newchat.QRCode
|
||||
import chat.simplex.res.MR
|
||||
|
||||
@Composable
|
||||
fun VerifyCodeView(
|
||||
@@ -61,14 +61,14 @@ private fun VerifyCodeLayout(
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(horizontal = DEFAULT_PADDING)
|
||||
) {
|
||||
AppBarTitle(stringResource(MR.strings.security_code), false)
|
||||
AppBarTitle(stringResource(R.string.security_code), false)
|
||||
val splitCode = splitToParts(connectionCode, 24)
|
||||
Row(Modifier.fillMaxWidth().padding(bottom = DEFAULT_PADDING_HALF), horizontalArrangement = Arrangement.Center) {
|
||||
if (connectionVerified) {
|
||||
Icon(painterResource(MR.images.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))
|
||||
Icon(painterResource(R.drawable.ic_verified_user), null, Modifier.padding(end = 4.dp).size(22.dp), tint = MaterialTheme.colors.secondary)
|
||||
Text(String.format(stringResource(R.string.is_verified), displayName))
|
||||
} 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
|
||||
)
|
||||
}
|
||||
val context = LocalContext.current
|
||||
Box(Modifier.weight(1f)) {
|
||||
IconButton({ shareText(connectionCode) }, Modifier.size(20.dp).align(Alignment.CenterStart)) {
|
||||
Icon(painterResource(MR.images.ic_share_filled), null, tint = MaterialTheme.colors.primary)
|
||||
IconButton({ shareText(context, connectionCode) }, Modifier.size(20.dp).align(Alignment.CenterStart)) {
|
||||
Icon(painterResource(R.drawable.ic_share_filled), null, tint = MaterialTheme.colors.primary)
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.weight(1f))
|
||||
}
|
||||
|
||||
Text(
|
||||
generalGetString(MR.strings.to_verify_compare),
|
||||
generalGetString(R.string.to_verify_compare),
|
||||
Modifier.padding(bottom = DEFAULT_PADDING)
|
||||
)
|
||||
|
||||
@@ -104,20 +105,20 @@ private fun VerifyCodeLayout(
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp)
|
||||
) {
|
||||
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) {}
|
||||
}
|
||||
} 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 {
|
||||
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 ->
|
||||
if (!verified) {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(MR.strings.incorrect_code)
|
||||
title = generalGetString(R.string.incorrect_code)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -11,16 +11,12 @@ import androidx.compose.foundation.*
|
||||
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.focus.FocusRequester
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -32,24 +28,21 @@ import chat.simplex.app.views.chat.ChatInfoToolbarTitle
|
||||
import chat.simplex.app.views.helpers.*
|
||||
import chat.simplex.app.views.newchat.InfoAboutIncognito
|
||||
import chat.simplex.app.views.usersettings.SettingsActionItem
|
||||
import chat.simplex.res.MR
|
||||
|
||||
@Composable
|
||||
fun AddGroupMembersView(groupInfo: GroupInfo, creatingGroup: Boolean = false, chatModel: ChatModel, close: () -> Unit) {
|
||||
val selectedContacts = remember { mutableStateListOf<Long>() }
|
||||
val selectedRole = remember { mutableStateOf(GroupMemberRole.Member) }
|
||||
var allowModifyMembers by remember { mutableStateOf(true) }
|
||||
val searchText = rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue("")) }
|
||||
BackHandler(onBack = close)
|
||||
AddGroupMembersLayout(
|
||||
chatModel.incognito.value,
|
||||
groupInfo = groupInfo,
|
||||
creatingGroup = creatingGroup,
|
||||
contactsToAdd = getContactsToAdd(chatModel, searchText.value.text),
|
||||
contactsToAdd = getContactsToAdd(chatModel),
|
||||
selectedContacts = selectedContacts,
|
||||
selectedRole = selectedRole,
|
||||
allowModifyMembers = allowModifyMembers,
|
||||
searchText,
|
||||
openPreferences = {
|
||||
ModalManager.shared.showCustomModal { close ->
|
||||
GroupPreferencesView(chatModel, groupInfo.id, close)
|
||||
@@ -76,8 +69,7 @@ fun AddGroupMembersView(groupInfo: GroupInfo, creatingGroup: Boolean = false, ch
|
||||
)
|
||||
}
|
||||
|
||||
fun getContactsToAdd(chatModel: ChatModel, search: String): List<Contact> {
|
||||
val s = search.trim().lowercase()
|
||||
fun getContactsToAdd(chatModel: ChatModel): List<Contact> {
|
||||
val memberContactIds = chatModel.groupMembers
|
||||
.filter { it.memberCurrent }
|
||||
.mapNotNull { it.memberContactId }
|
||||
@@ -86,7 +78,7 @@ fun getContactsToAdd(chatModel: ChatModel, search: String): List<Contact> {
|
||||
.map { it.chatInfo }
|
||||
.filterIsInstance<ChatInfo.Direct>()
|
||||
.map { it.contact }
|
||||
.filter { it.contactId !in memberContactIds && it.chatViewName.lowercase().contains(s) }
|
||||
.filter { it.contactId !in memberContactIds }
|
||||
.sortedBy { it.displayName.lowercase() }
|
||||
.toList()
|
||||
}
|
||||
@@ -100,7 +92,6 @@ fun AddGroupMembersLayout(
|
||||
selectedContacts: List<Long>,
|
||||
selectedRole: MutableState<GroupMemberRole>,
|
||||
allowModifyMembers: Boolean,
|
||||
searchText: MutableState<TextFieldValue>,
|
||||
openPreferences: () -> Unit,
|
||||
inviteMembers: () -> Unit,
|
||||
clearSelection: () -> Unit,
|
||||
@@ -113,12 +104,12 @@ fun AddGroupMembersLayout(
|
||||
.fillMaxWidth()
|
||||
.verticalScroll(rememberScrollState()),
|
||||
) {
|
||||
AppBarTitle(stringResource(MR.strings.button_add_members))
|
||||
AppBarTitle(stringResource(R.string.button_add_members))
|
||||
InfoAboutIncognito(
|
||||
chatModelIncognito,
|
||||
false,
|
||||
generalGetString(MR.strings.group_unsupported_incognito_main_profile_sent),
|
||||
generalGetString(MR.strings.group_main_profile_sent),
|
||||
generalGetString(R.string.group_unsupported_incognito_main_profile_sent),
|
||||
generalGetString(R.string.group_main_profile_sent),
|
||||
true
|
||||
)
|
||||
Spacer(Modifier.size(DEFAULT_PADDING))
|
||||
@@ -134,13 +125,13 @@ fun AddGroupMembersLayout(
|
||||
}
|
||||
SectionSpacer()
|
||||
|
||||
if (contactsToAdd.isEmpty() && searchText.value.text.isEmpty()) {
|
||||
if (contactsToAdd.isEmpty()) {
|
||||
Row(
|
||||
Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
Text(
|
||||
stringResource(MR.strings.no_contacts_to_add),
|
||||
stringResource(R.string.no_contacts_to_add),
|
||||
Modifier.padding(),
|
||||
color = MaterialTheme.colors.secondary
|
||||
)
|
||||
@@ -149,7 +140,7 @@ fun AddGroupMembersLayout(
|
||||
SectionView {
|
||||
if (creatingGroup) {
|
||||
SectionItemView(openPreferences) {
|
||||
Text(stringResource(MR.strings.set_group_preferences))
|
||||
Text(stringResource(R.string.set_group_preferences))
|
||||
}
|
||||
}
|
||||
RoleSelectionRow(groupInfo, selectedRole, allowModifyMembers)
|
||||
@@ -163,10 +154,8 @@ fun AddGroupMembersLayout(
|
||||
InviteSectionFooter(selectedContactsCount = selectedContacts.size, allowModifyMembers, clearSelection)
|
||||
}
|
||||
SectionDividerSpaced(maxTopPadding = true)
|
||||
SectionView(stringResource(MR.strings.select_contacts)) {
|
||||
SectionItemView(padding = PaddingValues(start = DEFAULT_PADDING, end = DEFAULT_PADDING_HALF)) {
|
||||
SearchRowView(searchText, selectedContacts.size)
|
||||
}
|
||||
|
||||
SectionView(stringResource(R.string.select_contacts)) {
|
||||
ContactList(contacts = contactsToAdd, selectedContacts, groupInfo, allowModifyMembers, addContact, removeContact)
|
||||
}
|
||||
}
|
||||
@@ -174,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
|
||||
private fun RoleSelectionRow(groupInfo: GroupInfo, selectedRole: MutableState<GroupMemberRole>, enabled: Boolean) {
|
||||
Row(
|
||||
@@ -202,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 }
|
||||
ExposedDropDownSettingRow(
|
||||
generalGetString(MR.strings.new_member_role),
|
||||
generalGetString(R.string.new_member_role),
|
||||
values,
|
||||
selectedRole,
|
||||
icon = null,
|
||||
@@ -214,8 +184,8 @@ private fun RoleSelectionRow(groupInfo: GroupInfo, selectedRole: MutableState<Gr
|
||||
@Composable
|
||||
fun InviteMembersButton(onClick: () -> Unit, disabled: Boolean) {
|
||||
SettingsActionItem(
|
||||
painterResource(MR.images.ic_check),
|
||||
stringResource(MR.strings.invite_to_group_button),
|
||||
painterResource(R.drawable.ic_check),
|
||||
stringResource(R.string.invite_to_group_button),
|
||||
click = onClick,
|
||||
textColor = MaterialTheme.colors.primary,
|
||||
iconColor = MaterialTheme.colors.primary,
|
||||
@@ -226,8 +196,8 @@ fun InviteMembersButton(onClick: () -> Unit, disabled: Boolean) {
|
||||
@Composable
|
||||
fun SkipInvitingButton(onClick: () -> Unit) {
|
||||
SettingsActionItem(
|
||||
painterResource(MR.images.ic_check),
|
||||
stringResource(MR.strings.skip_inviting_button),
|
||||
painterResource(R.drawable.ic_check),
|
||||
stringResource(R.string.skip_inviting_button),
|
||||
click = onClick,
|
||||
textColor = MaterialTheme.colors.primary,
|
||||
iconColor = MaterialTheme.colors.primary,
|
||||
@@ -243,7 +213,7 @@ fun InviteSectionFooter(selectedContactsCount: Int, enabled: Boolean, clearSelec
|
||||
) {
|
||||
if (selectedContactsCount >= 1) {
|
||||
Text(
|
||||
String.format(generalGetString(MR.strings.num_contacts_selected), selectedContactsCount),
|
||||
String.format(generalGetString(R.string.num_contacts_selected), selectedContactsCount),
|
||||
color = MaterialTheme.colors.secondary,
|
||||
fontSize = 12.sp
|
||||
)
|
||||
@@ -251,14 +221,14 @@ fun InviteSectionFooter(selectedContactsCount: Int, enabled: Boolean, clearSelec
|
||||
Modifier.clickable { if (enabled) clearSelection() }
|
||||
) {
|
||||
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,
|
||||
fontSize = 12.sp
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Text(
|
||||
stringResource(MR.strings.no_contacts_selected),
|
||||
stringResource(R.string.no_contacts_selected),
|
||||
color = MaterialTheme.colors.secondary,
|
||||
fontSize = 12.sp
|
||||
)
|
||||
@@ -299,13 +269,13 @@ fun ContactCheckRow(
|
||||
val icon: Painter
|
||||
val iconColor: Color
|
||||
if (prohibitedToInviteIncognito) {
|
||||
icon = painterResource(MR.images.ic_theater_comedy_filled)
|
||||
icon = painterResource(R.drawable.ic_theater_comedy_filled)
|
||||
iconColor = MaterialTheme.colors.secondary
|
||||
} 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
|
||||
} else {
|
||||
icon = painterResource(MR.images.ic_circle)
|
||||
icon = painterResource(R.drawable.ic_circle)
|
||||
iconColor = MaterialTheme.colors.secondary
|
||||
}
|
||||
SectionItemView(
|
||||
@@ -329,7 +299,7 @@ fun ContactCheckRow(
|
||||
Spacer(Modifier.fillMaxWidth().weight(1f))
|
||||
Icon(
|
||||
icon,
|
||||
contentDescription = stringResource(MR.strings.icon_descr_contact_checked),
|
||||
contentDescription = stringResource(R.string.icon_descr_contact_checked),
|
||||
tint = iconColor
|
||||
)
|
||||
}
|
||||
@@ -337,9 +307,9 @@ fun ContactCheckRow(
|
||||
|
||||
fun showProhibitedToInviteIncognitoAlertDialog() {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(MR.strings.invite_prohibited),
|
||||
text = generalGetString(MR.strings.invite_prohibited_description),
|
||||
confirmText = generalGetString(MR.strings.ok),
|
||||
title = generalGetString(R.string.invite_prohibited),
|
||||
text = generalGetString(R.string.invite_prohibited_description),
|
||||
confirmText = generalGetString(R.string.ok),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -355,7 +325,6 @@ fun PreviewAddGroupMembersLayout() {
|
||||
selectedContacts = remember { mutableStateListOf() },
|
||||
selectedRole = remember { mutableStateOf(GroupMemberRole.Admin) },
|
||||
allowModifyMembers = true,
|
||||
searchText = remember { mutableStateOf(TextFieldValue("")) },
|
||||
openPreferences = {},
|
||||
inviteMembers = {},
|
||||
clearSelection = {},
|
||||
@@ -13,14 +13,12 @@ import androidx.compose.foundation.*
|
||||
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.graphics.Color
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
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.FontWeight
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -34,7 +32,6 @@ import chat.simplex.app.views.chatlist.cantInviteIncognitoAlert
|
||||
import chat.simplex.app.views.chatlist.setGroupMembers
|
||||
import chat.simplex.app.views.helpers.*
|
||||
import chat.simplex.app.views.usersettings.*
|
||||
import chat.simplex.res.MR
|
||||
|
||||
@Composable
|
||||
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 ->
|
||||
withApi {
|
||||
val r = chatModel.controller.apiGroupMemberInfo(groupInfo.groupId, member.groupMemberId)
|
||||
val stats = r?.second
|
||||
val stats = chatModel.controller.apiGroupMemberInfo(groupInfo.groupId, member.groupMemberId)
|
||||
val (_, code) = if (member.memberActive) {
|
||||
try {
|
||||
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) {
|
||||
val alertTextKey =
|
||||
if (groupInfo.membership.memberCurrent) MR.strings.delete_group_for_all_members_cannot_undo_warning
|
||||
else MR.strings.delete_group_for_self_cannot_undo_warning
|
||||
if (groupInfo.membership.memberCurrent) R.string.delete_group_for_all_members_cannot_undo_warning
|
||||
else R.string.delete_group_for_self_cannot_undo_warning
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(MR.strings.delete_group_question),
|
||||
title = generalGetString(R.string.delete_group_question),
|
||||
text = generalGetString(alertTextKey),
|
||||
confirmText = generalGetString(MR.strings.delete_verb),
|
||||
confirmText = generalGetString(R.string.delete_verb),
|
||||
onConfirm = {
|
||||
withApi {
|
||||
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) {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(MR.strings.leave_group_question),
|
||||
text = generalGetString(MR.strings.you_will_stop_receiving_messages_from_this_group_chat_history_will_be_preserved),
|
||||
confirmText = generalGetString(MR.strings.leave_group_button),
|
||||
title = generalGetString(R.string.leave_group_question),
|
||||
text = generalGetString(R.string.you_will_stop_receiving_messages_from_this_group_chat_history_will_be_preserved),
|
||||
confirmText = generalGetString(R.string.leave_group_button),
|
||||
onConfirm = {
|
||||
withApi {
|
||||
chatModel.controller.leaveGroup(groupInfo.groupId)
|
||||
@@ -179,16 +175,14 @@ fun GroupChatInfoLayout(
|
||||
SectionView {
|
||||
if (groupInfo.canEdit) {
|
||||
EditGroupProfileButton(editGroupProfile)
|
||||
}
|
||||
if (groupInfo.groupProfile.description != null || groupInfo.canEdit) {
|
||||
AddOrEditWelcomeMessage(groupInfo.groupProfile.description, addOrEditWelcomeMessage)
|
||||
}
|
||||
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)
|
||||
|
||||
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 (groupLink == null) {
|
||||
CreateGroupLinkButton(manageGroupLink)
|
||||
@@ -200,17 +194,10 @@ fun GroupChatInfoLayout(
|
||||
val tint = if (chat.chatInfo.incognito) MaterialTheme.colors.secondary else MaterialTheme.colors.primary
|
||||
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) {
|
||||
MemberRow(groupInfo.membership, user = true)
|
||||
}
|
||||
MembersList(filteredMembers.value, showMemberInfo)
|
||||
MembersList(members, showMemberInfo)
|
||||
}
|
||||
SectionDividerSpaced(maxTopPadding = true, maxBottomPadding = false)
|
||||
SectionView {
|
||||
@@ -225,9 +212,9 @@ fun GroupChatInfoLayout(
|
||||
|
||||
if (developerTools) {
|
||||
SectionDividerSpaced()
|
||||
SectionView(title = stringResource(MR.strings.section_title_for_console)) {
|
||||
InfoRow(stringResource(MR.strings.info_row_local_name), groupInfo.localDisplayName)
|
||||
InfoRow(stringResource(MR.strings.info_row_database_id), groupInfo.apiId.toString())
|
||||
SectionView(title = stringResource(R.string.section_title_for_console)) {
|
||||
InfoRow(stringResource(R.string.info_row_local_name), groupInfo.localDisplayName)
|
||||
InfoRow(stringResource(R.string.info_row_database_id), groupInfo.apiId.toString())
|
||||
}
|
||||
}
|
||||
SectionBottomSpacer()
|
||||
@@ -261,8 +248,8 @@ private fun GroupChatInfoHeader(cInfo: ChatInfo) {
|
||||
@Composable
|
||||
private fun GroupPreferencesButton(onClick: () -> Unit) {
|
||||
SettingsActionItem(
|
||||
painterResource(MR.images.ic_toggle_on),
|
||||
stringResource(MR.strings.group_preferences),
|
||||
painterResource(R.drawable.ic_toggle_on),
|
||||
stringResource(R.string.group_preferences),
|
||||
click = onClick
|
||||
)
|
||||
}
|
||||
@@ -270,8 +257,8 @@ private fun GroupPreferencesButton(onClick: () -> Unit) {
|
||||
@Composable
|
||||
private fun AddMembersButton(tint: Color = MaterialTheme.colors.primary, onClick: () -> Unit) {
|
||||
SettingsActionItem(
|
||||
painterResource(MR.images.ic_add),
|
||||
stringResource(MR.strings.button_add_members),
|
||||
painterResource(R.drawable.ic_add),
|
||||
stringResource(R.string.button_add_members),
|
||||
onClick,
|
||||
iconColor = tint,
|
||||
textColor = tint
|
||||
@@ -315,7 +302,7 @@ private fun MemberRow(member: GroupMember, user: Boolean = false) {
|
||||
)
|
||||
}
|
||||
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(
|
||||
statusDescr,
|
||||
color = MaterialTheme.colors.secondary,
|
||||
@@ -334,14 +321,14 @@ private fun MemberRow(member: GroupMember, user: Boolean = false) {
|
||||
|
||||
@Composable
|
||||
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
|
||||
private fun GroupLinkButton(onClick: () -> Unit) {
|
||||
SettingsActionItem(
|
||||
painterResource(MR.images.ic_link),
|
||||
stringResource(MR.strings.group_link),
|
||||
painterResource(R.drawable.ic_link),
|
||||
stringResource(R.string.group_link),
|
||||
onClick,
|
||||
iconColor = MaterialTheme.colors.secondary
|
||||
)
|
||||
@@ -350,8 +337,8 @@ private fun GroupLinkButton(onClick: () -> Unit) {
|
||||
@Composable
|
||||
private fun CreateGroupLinkButton(onClick: () -> Unit) {
|
||||
SettingsActionItem(
|
||||
painterResource(MR.images.ic_add_link),
|
||||
stringResource(MR.strings.create_group_link),
|
||||
painterResource(R.drawable.ic_add_link),
|
||||
stringResource(R.string.create_group_link),
|
||||
onClick,
|
||||
iconColor = MaterialTheme.colors.secondary
|
||||
)
|
||||
@@ -360,8 +347,8 @@ private fun CreateGroupLinkButton(onClick: () -> Unit) {
|
||||
@Composable
|
||||
fun EditGroupProfileButton(onClick: () -> Unit) {
|
||||
SettingsActionItem(
|
||||
painterResource(MR.images.ic_edit),
|
||||
stringResource(MR.strings.button_edit_group_profile),
|
||||
painterResource(R.drawable.ic_edit),
|
||||
stringResource(R.string.button_edit_group_profile),
|
||||
onClick,
|
||||
iconColor = MaterialTheme.colors.secondary
|
||||
)
|
||||
@@ -370,12 +357,12 @@ fun EditGroupProfileButton(onClick: () -> Unit) {
|
||||
@Composable
|
||||
private fun AddOrEditWelcomeMessage(welcomeMessage: String?, onClick: () -> Unit) {
|
||||
val text = if (welcomeMessage == null) {
|
||||
stringResource(MR.strings.button_add_welcome_message)
|
||||
stringResource(R.string.button_add_welcome_message)
|
||||
} else {
|
||||
stringResource(MR.strings.button_welcome_message)
|
||||
stringResource(R.string.button_welcome_message)
|
||||
}
|
||||
SettingsActionItem(
|
||||
painterResource(MR.images.ic_maps_ugc),
|
||||
painterResource(R.drawable.ic_maps_ugc),
|
||||
text,
|
||||
onClick,
|
||||
iconColor = MaterialTheme.colors.secondary
|
||||
@@ -385,8 +372,8 @@ private fun AddOrEditWelcomeMessage(welcomeMessage: String?, onClick: () -> Unit
|
||||
@Composable
|
||||
private fun LeaveGroupButton(onClick: () -> Unit) {
|
||||
SettingsActionItem(
|
||||
painterResource(MR.images.ic_logout),
|
||||
stringResource(MR.strings.button_leave_group),
|
||||
painterResource(R.drawable.ic_logout),
|
||||
stringResource(R.string.button_leave_group),
|
||||
onClick,
|
||||
iconColor = Color.Red,
|
||||
textColor = Color.Red
|
||||
@@ -396,27 +383,14 @@ private fun LeaveGroupButton(onClick: () -> Unit) {
|
||||
@Composable
|
||||
private fun DeleteGroupButton(onClick: () -> Unit) {
|
||||
SettingsActionItem(
|
||||
painterResource(MR.images.ic_delete),
|
||||
stringResource(MR.strings.button_delete_group),
|
||||
painterResource(R.drawable.ic_delete),
|
||||
stringResource(R.string.button_delete_group),
|
||||
onClick,
|
||||
iconColor = 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
|
||||
@Composable
|
||||
fun PreviewGroupChatInfoLayout() {
|
||||
@@ -10,8 +10,9 @@ import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.app.R
|
||||
@@ -19,13 +20,13 @@ import chat.simplex.app.model.*
|
||||
import chat.simplex.app.ui.theme.*
|
||||
import chat.simplex.app.views.helpers.*
|
||||
import chat.simplex.app.views.newchat.QRCode
|
||||
import chat.simplex.res.MR
|
||||
|
||||
@Composable
|
||||
fun GroupLinkView(chatModel: ChatModel, groupInfo: GroupInfo, connReqContact: String?, memberRole: GroupMemberRole?, onGroupLinkUpdated: (Pair<String?, GroupMemberRole?>) -> Unit) {
|
||||
var groupLink by rememberSaveable { mutableStateOf(connReqContact) }
|
||||
val groupLinkMemberRole = rememberSaveable { mutableStateOf(memberRole) }
|
||||
var creatingLink by rememberSaveable { mutableStateOf(false) }
|
||||
val cxt = LocalContext.current
|
||||
fun createLink() {
|
||||
creatingLink = true
|
||||
withApi {
|
||||
@@ -49,7 +50,7 @@ fun GroupLinkView(chatModel: ChatModel, groupInfo: GroupInfo, connReqContact: St
|
||||
groupLinkMemberRole,
|
||||
creatingLink,
|
||||
createLink = ::createLink,
|
||||
share = { shareText(groupLink ?: return@GroupLinkLayout) },
|
||||
share = { shareText(cxt, groupLink ?: return@GroupLinkLayout) },
|
||||
updateLink = {
|
||||
val role = groupLinkMemberRole.value
|
||||
if (role != null) {
|
||||
@@ -65,9 +66,9 @@ fun GroupLinkView(chatModel: ChatModel, groupInfo: GroupInfo, connReqContact: St
|
||||
},
|
||||
deleteLink = {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(MR.strings.delete_link_question),
|
||||
text = generalGetString(MR.strings.all_group_members_will_remain_connected),
|
||||
confirmText = generalGetString(MR.strings.delete_verb),
|
||||
title = generalGetString(R.string.delete_link_question),
|
||||
text = generalGetString(R.string.all_group_members_will_remain_connected),
|
||||
confirmText = generalGetString(R.string.delete_verb),
|
||||
onConfirm = {
|
||||
withApi {
|
||||
val r = chatModel.controller.apiDeleteGroupLink(groupInfo.groupId)
|
||||
@@ -101,9 +102,9 @@ fun GroupLinkLayout(
|
||||
Modifier
|
||||
.verticalScroll(rememberScrollState()),
|
||||
) {
|
||||
AppBarTitle(stringResource(MR.strings.group_link))
|
||||
AppBarTitle(stringResource(R.string.group_link))
|
||||
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),
|
||||
lineHeight = 22.sp
|
||||
)
|
||||
@@ -113,7 +114,7 @@ fun GroupLinkLayout(
|
||||
verticalArrangement = Arrangement.SpaceEvenly
|
||||
) {
|
||||
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 {
|
||||
RoleSelectionRow(groupInfo, groupLinkMemberRole)
|
||||
var initialLaunch by remember { mutableStateOf(true) }
|
||||
@@ -130,13 +131,13 @@ fun GroupLinkLayout(
|
||||
modifier = Modifier.padding(horizontal = DEFAULT_PADDING, vertical = 10.dp)
|
||||
) {
|
||||
SimpleButton(
|
||||
stringResource(MR.strings.share_link),
|
||||
icon = painterResource(MR.images.ic_share),
|
||||
stringResource(R.string.share_link),
|
||||
icon = painterResource(R.drawable.ic_share),
|
||||
click = share
|
||||
)
|
||||
SimpleButton(
|
||||
stringResource(MR.strings.delete_link),
|
||||
icon = painterResource(MR.images.ic_delete),
|
||||
stringResource(R.string.delete_link),
|
||||
icon = painterResource(R.drawable.ic_delete),
|
||||
color = Color.Red,
|
||||
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 }
|
||||
ExposedDropDownSettingRow(
|
||||
generalGetString(MR.strings.initial_member_role),
|
||||
generalGetString(R.string.initial_member_role),
|
||||
values,
|
||||
selectedRole,
|
||||
icon = null,
|
||||
@@ -4,10 +4,7 @@ import InfoRow
|
||||
import SectionBottomSpacer
|
||||
import SectionDividerSpaced
|
||||
import SectionSpacer
|
||||
import SectionTextFooter
|
||||
import SectionView
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
@@ -16,29 +13,25 @@ import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
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.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.*
|
||||
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.newchat.*
|
||||
import chat.simplex.app.views.usersettings.SettingsActionItem
|
||||
import chat.simplex.res.MR
|
||||
import kotlinx.datetime.Clock
|
||||
|
||||
@Composable
|
||||
fun GroupMemberInfoView(
|
||||
groupInfo: GroupInfo,
|
||||
member: GroupMember,
|
||||
connectionStats: ConnectionStats?,
|
||||
connStats: ConnectionStats?,
|
||||
connectionCode: String?,
|
||||
chatModel: ChatModel,
|
||||
close: () -> Unit,
|
||||
@@ -46,7 +39,6 @@ fun GroupMemberInfoView(
|
||||
) {
|
||||
BackHandler(onBack = close)
|
||||
val chat = chatModel.chats.firstOrNull { it.id == chatModel.chatId.value }
|
||||
val connStats = remember { mutableStateOf(connectionStats) }
|
||||
val developerTools = chatModel.controller.appPrefs.developerTools.get()
|
||||
if (chat != null) {
|
||||
val newRole = remember { mutableStateOf(member.memberRole) }
|
||||
@@ -72,15 +64,6 @@ fun GroupMemberInfoView(
|
||||
}
|
||||
}
|
||||
},
|
||||
connectViaAddress = { connReqUri ->
|
||||
val uri = Uri.parse(connReqUri)
|
||||
withUriAction(uri) { linkType ->
|
||||
withApi {
|
||||
Log.d(TAG, "connectViaUri: connecting")
|
||||
connectViaUri(chatModel, linkType, uri)
|
||||
}
|
||||
}
|
||||
},
|
||||
removeMember = { removeMemberDialog(groupInfo, member, chatModel, close) },
|
||||
onRoleSelected = {
|
||||
if (it == newRole.value) return@GroupMemberInfoLayout
|
||||
@@ -100,50 +83,7 @@ fun GroupMemberInfoView(
|
||||
}
|
||||
},
|
||||
switchMemberAddress = {
|
||||
showSwitchAddressAlert(switchAddress = {
|
||||
withApi {
|
||||
val r = chatModel.controller.apiSwitchGroupMember(groupInfo.apiId, member.groupMemberId)
|
||||
if (r != null) {
|
||||
connStats.value = r.second
|
||||
chatModel.updateGroupMemberConnectionStats(groupInfo, r.first, r.second)
|
||||
close.invoke()
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
abortSwitchMemberAddress = {
|
||||
showAbortSwitchAddressAlert(abortSwitchAddress = {
|
||||
withApi {
|
||||
val r = chatModel.controller.apiAbortSwitchGroupMember(groupInfo.apiId, member.groupMemberId)
|
||||
if (r != null) {
|
||||
connStats.value = r.second
|
||||
chatModel.updateGroupMemberConnectionStats(groupInfo, r.first, r.second)
|
||||
close.invoke()
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
syncMemberConnection = {
|
||||
withApi {
|
||||
val r = chatModel.controller.apiSyncGroupMemberRatchet(groupInfo.apiId, member.groupMemberId, force = false)
|
||||
if (r != null) {
|
||||
connStats.value = r.second
|
||||
chatModel.updateGroupMemberConnectionStats(groupInfo, r.first, r.second)
|
||||
close.invoke()
|
||||
}
|
||||
}
|
||||
},
|
||||
syncMemberConnectionForce = {
|
||||
showSyncConnectionForceAlert(syncConnectionForce = {
|
||||
withApi {
|
||||
val r = chatModel.controller.apiSyncGroupMemberRatchet(groupInfo.apiId, member.groupMemberId, force = true)
|
||||
if (r != null) {
|
||||
connStats.value = r.second
|
||||
chatModel.updateGroupMemberConnectionStats(groupInfo, r.first, r.second)
|
||||
close.invoke()
|
||||
}
|
||||
}
|
||||
})
|
||||
switchMemberAddress(chatModel, groupInfo, member)
|
||||
},
|
||||
verifyClicked = {
|
||||
ModalManager.shared.showModalCloseable { close ->
|
||||
@@ -177,9 +117,9 @@ fun GroupMemberInfoView(
|
||||
|
||||
fun removeMemberDialog(groupInfo: GroupInfo, member: GroupMember, chatModel: ChatModel, close: (() -> Unit)? = null) {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(MR.strings.button_remove_member),
|
||||
text = generalGetString(MR.strings.member_will_be_removed_from_group_cannot_be_undone),
|
||||
confirmText = generalGetString(MR.strings.remove_member_confirmation),
|
||||
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)
|
||||
@@ -197,31 +137,17 @@ fun removeMemberDialog(groupInfo: GroupInfo, member: GroupMember, chatModel: Cha
|
||||
fun GroupMemberInfoLayout(
|
||||
groupInfo: GroupInfo,
|
||||
member: GroupMember,
|
||||
connStats: MutableState<ConnectionStats?>,
|
||||
connStats: ConnectionStats?,
|
||||
newRole: MutableState<GroupMemberRole>,
|
||||
developerTools: Boolean,
|
||||
connectionCode: String?,
|
||||
getContactChat: (Long) -> Chat?,
|
||||
openDirectChat: (Long) -> Unit,
|
||||
connectViaAddress: (String) -> Unit,
|
||||
removeMember: () -> Unit,
|
||||
onRoleSelected: (GroupMemberRole) -> Unit,
|
||||
switchMemberAddress: () -> Unit,
|
||||
abortSwitchMemberAddress: () -> Unit,
|
||||
syncMemberConnection: () -> Unit,
|
||||
syncMemberConnectionForce: () -> Unit,
|
||||
verifyClicked: () -> Unit,
|
||||
) {
|
||||
val cStats = connStats.value
|
||||
fun knownDirectChat(contactId: Long): Chat? {
|
||||
val chat = getContactChat(contactId)
|
||||
return if (chat != null && chat.chatInfo is ChatInfo.Direct && chat.chatInfo.contact.directOrUsed) {
|
||||
chat
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
@@ -235,80 +161,53 @@ fun GroupMemberInfoLayout(
|
||||
}
|
||||
SectionSpacer()
|
||||
|
||||
val contactId = member.memberContactId
|
||||
|
||||
if (member.memberActive) {
|
||||
val contactId = member.memberContactId
|
||||
if (contactId != null) {
|
||||
SectionView {
|
||||
if (knownDirectChat(contactId) != null || groupInfo.fullGroupPreferences.directMessages.on) {
|
||||
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)
|
||||
}
|
||||
if (cStats != null && cStats.ratchetSyncAllowed) {
|
||||
SynchronizeConnectionButton(syncMemberConnection)
|
||||
} else if (developerTools) {
|
||||
SynchronizeConnectionButtonForce(syncMemberConnectionForce)
|
||||
}
|
||||
}
|
||||
SectionDividerSpaced()
|
||||
SectionSpacer()
|
||||
}
|
||||
}
|
||||
|
||||
if (member.contactLink != null) {
|
||||
val context = LocalContext.current
|
||||
SectionView(stringResource(MR.strings.address_section_title).uppercase()) {
|
||||
QRCode(member.contactLink, Modifier.padding(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF).aspectRatio(1f))
|
||||
ShareAddressButton { shareText(member.contactLink) }
|
||||
if (contactId != null) {
|
||||
if (knownDirectChat(contactId) == null && !groupInfo.fullGroupPreferences.directMessages.on) {
|
||||
ConnectViaAddressButton(onClick = { connectViaAddress(member.contactLink) })
|
||||
}
|
||||
} else {
|
||||
ConnectViaAddressButton(onClick = { connectViaAddress(member.contactLink) })
|
||||
}
|
||||
SectionTextFooter(stringResource(MR.strings.you_can_share_this_address_with_your_contacts).format(member.displayName))
|
||||
}
|
||||
SectionDividerSpaced()
|
||||
}
|
||||
|
||||
SectionView(title = stringResource(MR.strings.member_info_section_title_member)) {
|
||||
InfoRow(stringResource(MR.strings.info_row_group), groupInfo.displayName)
|
||||
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(MR.strings.role_in_group), member.memberRole.text)
|
||||
InfoRow(stringResource(R.string.role_in_group), member.memberRole.text)
|
||||
}
|
||||
val conn = member.activeConn
|
||||
if (conn != null) {
|
||||
val connLevelDesc =
|
||||
if (conn.connLevel == 0) stringResource(MR.strings.conn_level_desc_direct)
|
||||
else String.format(generalGetString(MR.strings.conn_level_desc_indirect), conn.connLevel)
|
||||
InfoRow(stringResource(MR.strings.info_row_connection), 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 (cStats != null) {
|
||||
if (connStats != null) {
|
||||
SectionDividerSpaced()
|
||||
SectionView(title = stringResource(MR.strings.conn_stats_section_title_servers)) {
|
||||
SwitchAddressButton(
|
||||
disabled = cStats.rcvQueuesInfo.any { it.rcvSwitchStatus != null } || cStats.ratchetSyncSendProhibited,
|
||||
switchAddress = switchMemberAddress
|
||||
)
|
||||
if (cStats.rcvQueuesInfo.any { it.rcvSwitchStatus != null }) {
|
||||
AbortSwitchAddressButton(
|
||||
disabled = cStats.rcvQueuesInfo.any { it.rcvSwitchStatus != null && !it.canAbortSwitch } || cStats.ratchetSyncSendProhibited,
|
||||
abortSwitchAddress = abortSwitchMemberAddress
|
||||
)
|
||||
}
|
||||
val rcvServers = cStats.rcvQueuesInfo.map { it.rcvServer }
|
||||
if (rcvServers.isNotEmpty()) {
|
||||
SimplexServers(stringResource(MR.strings.receiving_via), rcvServers)
|
||||
}
|
||||
val sndServers = cStats.sndQueuesInfo.map { it.sndServer }
|
||||
if (sndServers.isNotEmpty()) {
|
||||
SimplexServers(stringResource(MR.strings.sending_via), sndServers)
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -322,9 +221,9 @@ fun GroupMemberInfoLayout(
|
||||
|
||||
if (developerTools) {
|
||||
SectionDividerSpaced()
|
||||
SectionView(title = stringResource(MR.strings.section_title_for_console)) {
|
||||
InfoRow(stringResource(MR.strings.info_row_local_name), member.localDisplayName)
|
||||
InfoRow(stringResource(MR.strings.info_row_database_id), member.groupMemberId.toString())
|
||||
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()
|
||||
@@ -340,7 +239,7 @@ fun GroupMemberInfoHeader(member: GroupMember) {
|
||||
ProfileImage(size = 192.dp, member.image, color = if (isInDarkTheme()) GroupDark else SettingsSecondaryLight)
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
if (member.verified) {
|
||||
Icon(painterResource(MR.images.ic_verified_user), null, Modifier.padding(end = 6.dp, top = 4.dp).size(24.dp), tint = MaterialTheme.colors.secondary)
|
||||
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),
|
||||
@@ -363,8 +262,8 @@ fun GroupMemberInfoHeader(member: GroupMember) {
|
||||
@Composable
|
||||
fun RemoveMemberButton(onClick: () -> Unit) {
|
||||
SettingsActionItem(
|
||||
painterResource(MR.images.ic_delete),
|
||||
stringResource(MR.strings.button_remove_member),
|
||||
painterResource(R.drawable.ic_delete),
|
||||
stringResource(R.string.button_remove_member),
|
||||
click = onClick,
|
||||
textColor = Color.Red,
|
||||
iconColor = Color.Red,
|
||||
@@ -374,19 +273,8 @@ fun RemoveMemberButton(onClick: () -> Unit) {
|
||||
@Composable
|
||||
fun OpenChatButton(onClick: () -> Unit) {
|
||||
SettingsActionItem(
|
||||
painterResource(MR.images.ic_chat),
|
||||
stringResource(MR.strings.button_send_direct_message),
|
||||
click = onClick,
|
||||
textColor = MaterialTheme.colors.primary,
|
||||
iconColor = MaterialTheme.colors.primary,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ConnectViaAddressButton(onClick: () -> Unit) {
|
||||
SettingsActionItem(
|
||||
painterResource(MR.images.ic_link),
|
||||
stringResource(MR.strings.connect_button),
|
||||
painterResource(R.drawable.ic_chat),
|
||||
stringResource(R.string.button_send_direct_message),
|
||||
click = onClick,
|
||||
textColor = MaterialTheme.colors.primary,
|
||||
iconColor = MaterialTheme.colors.primary,
|
||||
@@ -406,7 +294,7 @@ private fun RoleSelectionRow(
|
||||
) {
|
||||
val values = remember { roles.map { it to it.text } }
|
||||
ExposedDropDownSettingRow(
|
||||
generalGetString(MR.strings.change_role),
|
||||
generalGetString(R.string.change_role),
|
||||
values,
|
||||
selectedRole,
|
||||
icon = null,
|
||||
@@ -423,18 +311,22 @@ private fun updateMemberRoleDialog(
|
||||
onConfirm: () -> Unit
|
||||
) {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(MR.strings.change_member_role_question),
|
||||
title = generalGetString(R.string.change_member_role_question),
|
||||
text = if (member.memberCurrent)
|
||||
String.format(generalGetString(MR.strings.member_role_will_be_changed_with_notification), newRole.text)
|
||||
String.format(generalGetString(R.string.member_role_will_be_changed_with_notification), newRole.text)
|
||||
else
|
||||
String.format(generalGetString(MR.strings.member_role_will_be_changed_with_invitation), newRole.text),
|
||||
confirmText = generalGetString(MR.strings.change_verb),
|
||||
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() {
|
||||
@@ -442,19 +334,15 @@ fun PreviewGroupMemberInfoLayout() {
|
||||
GroupMemberInfoLayout(
|
||||
groupInfo = GroupInfo.sampleData,
|
||||
member = GroupMember.sampleData,
|
||||
connStats = remember { mutableStateOf(null) },
|
||||
connStats = null,
|
||||
newRole = remember { mutableStateOf(GroupMemberRole.Member) },
|
||||
developerTools = false,
|
||||
connectionCode = "123",
|
||||
getContactChat = { Chat.sampleData },
|
||||
openDirectChat = {},
|
||||
connectViaAddress = {},
|
||||
removeMember = {},
|
||||
onRoleSelected = {},
|
||||
switchMemberAddress = {},
|
||||
abortSwitchMemberAddress = {},
|
||||
syncMemberConnection = {},
|
||||
syncMemberConnectionForce = {},
|
||||
verifyClicked = {},
|
||||
)
|
||||
}
|
||||
@@ -13,13 +13,13 @@ import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
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.model.*
|
||||
import chat.simplex.app.ui.theme.*
|
||||
import chat.simplex.app.views.chat.TimedMessagesTTLPicker
|
||||
import chat.simplex.app.views.helpers.*
|
||||
import chat.simplex.app.views.usersettings.PreferenceToggleWithIcon
|
||||
import chat.simplex.res.MR
|
||||
|
||||
@Composable
|
||||
fun GroupPreferencesView(m: ChatModel, chatId: String, close: () -> Unit,) {
|
||||
@@ -31,9 +31,9 @@ fun GroupPreferencesView(m: ChatModel, chatId: String, close: () -> Unit,) {
|
||||
fun savePrefs(afterSave: () -> Unit = {}) {
|
||||
withApi {
|
||||
val gp = gInfo.groupProfile.copy(groupPreferences = preferences.toGroupPreferences())
|
||||
val g = m.controller.apiUpdateGroup(gInfo.groupId, gp)
|
||||
if (g != null) {
|
||||
m.updateGroup(g)
|
||||
val gInfo = m.controller.apiUpdateGroup(gInfo.groupId, gp)
|
||||
if (gInfo != null) {
|
||||
m.updateGroup(gInfo)
|
||||
currentPreferences = preferences
|
||||
}
|
||||
afterSave()
|
||||
@@ -72,14 +72,14 @@ private fun GroupPreferencesLayout(
|
||||
Column(
|
||||
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 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 ->
|
||||
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 {
|
||||
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)))
|
||||
}
|
||||
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) }
|
||||
FeatureSection(GroupFeature.Voice, allowVoice, groupInfo, preferences, onTTLUpdated) {
|
||||
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) {
|
||||
SectionDividerSpaced(maxTopPadding = true, maxBottomPadding = false)
|
||||
ResetSaveButtons(
|
||||
@@ -147,15 +136,7 @@ private fun FeatureSection(
|
||||
}
|
||||
if (timedOn) {
|
||||
val ttl = rememberSaveable(preferences.timedMessages) { mutableStateOf(preferences.timedMessages.ttl) }
|
||||
DropdownCustomTimePickerSettingRow(
|
||||
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
|
||||
)
|
||||
TimedMessagesTTLPicker(ttl, onTTLUpdated)
|
||||
}
|
||||
} else {
|
||||
InfoRow(
|
||||
@@ -165,7 +146,7 @@ private fun FeatureSection(
|
||||
iconTint = iconTint,
|
||||
)
|
||||
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) {
|
||||
SectionView {
|
||||
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) {
|
||||
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) {
|
||||
AlertManager.shared.showAlertDialogStacked(
|
||||
title = generalGetString(MR.strings.save_preferences_question),
|
||||
confirmText = generalGetString(MR.strings.save_and_notify_group_members),
|
||||
dismissText = generalGetString(MR.strings.exit_without_saving),
|
||||
title = generalGetString(R.string.save_preferences_question),
|
||||
confirmText = generalGetString(R.string.save_and_notify_group_members),
|
||||
dismissText = generalGetString(R.string.exit_without_saving),
|
||||
onConfirm = save,
|
||||
onDismiss = revert,
|
||||
)
|
||||
@@ -13,7 +13,7 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
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.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -28,7 +28,6 @@ import chat.simplex.app.views.onboarding.ReadableText
|
||||
import chat.simplex.app.views.usersettings.*
|
||||
import com.google.accompanist.insets.ProvideWindowInsets
|
||||
import com.google.accompanist.insets.navigationBarsWithImePadding
|
||||
import chat.simplex.res.MR
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@@ -106,7 +105,7 @@ fun GroupProfileLayout(
|
||||
Modifier.fillMaxWidth()
|
||||
.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(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
@@ -125,13 +124,13 @@ fun GroupProfileLayout(
|
||||
}
|
||||
Row(Modifier.padding(bottom = DEFAULT_PADDING_HALF).fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
|
||||
Text(
|
||||
stringResource(MR.strings.group_display_name_field),
|
||||
stringResource(R.string.group_display_name_field),
|
||||
fontSize = 16.sp
|
||||
)
|
||||
if (!isValidDisplayName(displayName.value)) {
|
||||
Spacer(Modifier.size(DEFAULT_PADDING_HALF))
|
||||
Text(
|
||||
stringResource(MR.strings.no_spaces),
|
||||
stringResource(R.string.no_spaces),
|
||||
fontSize = 16.sp,
|
||||
color = Color.Red
|
||||
)
|
||||
@@ -140,7 +139,7 @@ fun GroupProfileLayout(
|
||||
ProfileNameField(displayName, "", ::isValidDisplayName, focusRequester)
|
||||
Spacer(Modifier.height(DEFAULT_PADDING))
|
||||
Text(
|
||||
stringResource(MR.strings.group_full_name_field),
|
||||
stringResource(R.string.group_full_name_field),
|
||||
fontSize = 16.sp,
|
||||
modifier = Modifier.padding(bottom = DEFAULT_PADDING_HALF)
|
||||
)
|
||||
@@ -149,7 +148,7 @@ fun GroupProfileLayout(
|
||||
val enabled = !dataUnchanged && displayName.value.isNotEmpty() && isValidDisplayName(displayName.value)
|
||||
if (enabled) {
|
||||
Text(
|
||||
stringResource(MR.strings.save_group_profile),
|
||||
stringResource(R.string.save_group_profile),
|
||||
modifier = Modifier.clickable {
|
||||
saveProfile(
|
||||
groupProfile.copy(
|
||||
@@ -163,7 +162,7 @@ fun GroupProfileLayout(
|
||||
)
|
||||
} else {
|
||||
Text(
|
||||
stringResource(MR.strings.save_group_profile),
|
||||
stringResource(R.string.save_group_profile),
|
||||
color = MaterialTheme.colors.secondary
|
||||
)
|
||||
}
|
||||
@@ -183,9 +182,9 @@ fun GroupProfileLayout(
|
||||
|
||||
private fun showUnsavedChangesAlert(save: () -> Unit, revert: () -> Unit) {
|
||||
AlertManager.shared.showAlertDialogStacked(
|
||||
title = generalGetString(MR.strings.save_preferences_question),
|
||||
confirmText = generalGetString(MR.strings.save_and_notify_group_members),
|
||||
dismissText = generalGetString(MR.strings.exit_without_saving),
|
||||
title = generalGetString(R.string.save_preferences_question),
|
||||
confirmText = generalGetString(R.string.save_and_notify_group_members),
|
||||
dismissText = generalGetString(R.string.exit_without_saving),
|
||||
onConfirm = save,
|
||||
onDismiss = revert,
|
||||
)
|
||||
@@ -0,0 +1,95 @@
|
||||
package chat.simplex.app.views.chat.group
|
||||
|
||||
import SectionBottomSpacer
|
||||
import SectionItemView
|
||||
import SectionSpacer
|
||||
import SectionView
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.model.*
|
||||
import chat.simplex.app.ui.theme.*
|
||||
import chat.simplex.app.views.helpers.*
|
||||
|
||||
@Composable
|
||||
fun GroupWelcomeView(m: ChatModel, groupInfo: GroupInfo, close: () -> Unit) {
|
||||
var groupInfo by remember { mutableStateOf(groupInfo) }
|
||||
val welcomeText = remember { mutableStateOf(groupInfo.groupProfile.description ?: "") }
|
||||
|
||||
fun save(afterSave: () -> Unit = {}) {
|
||||
withApi {
|
||||
var welcome: String? = welcomeText.value.trim('\n', ' ')
|
||||
if (welcome?.length == 0) {
|
||||
welcome = null
|
||||
}
|
||||
val groupProfileUpdated = groupInfo.groupProfile.copy(description = welcome)
|
||||
val res = m.controller.apiUpdateGroup(groupInfo.groupId, groupProfileUpdated)
|
||||
if (res != null) {
|
||||
groupInfo = res
|
||||
m.updateGroup(res)
|
||||
welcomeText.value = welcome ?: ""
|
||||
}
|
||||
afterSave()
|
||||
}
|
||||
}
|
||||
|
||||
ModalView(
|
||||
close = {
|
||||
if (welcomeText.value == groupInfo.groupProfile.description || (welcomeText.value == "" && groupInfo.groupProfile.description == null)) close()
|
||||
else showUnsavedChangesAlert({ save(close) }, close)
|
||||
},
|
||||
) {
|
||||
GroupWelcomeLayout(
|
||||
welcomeText,
|
||||
groupInfo,
|
||||
save = ::save
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun GroupWelcomeLayout(
|
||||
welcomeText: MutableState<String>,
|
||||
groupInfo: GroupInfo,
|
||||
save: () -> Unit,
|
||||
) {
|
||||
Column(
|
||||
Modifier.fillMaxWidth().verticalScroll(rememberScrollState()),
|
||||
) {
|
||||
AppBarTitle(stringResource(R.string.group_welcome_title))
|
||||
val welcomeText = remember { welcomeText }
|
||||
TextEditor(Modifier.padding(horizontal = DEFAULT_PADDING).height(160.dp), text = welcomeText)
|
||||
SectionSpacer()
|
||||
SaveButton(
|
||||
save = save,
|
||||
disabled = welcomeText.value == groupInfo.groupProfile.description || (welcomeText.value == "" && groupInfo.groupProfile.description == null)
|
||||
)
|
||||
SectionBottomSpacer()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SaveButton(save: () -> Unit, disabled: Boolean) {
|
||||
SectionView {
|
||||
SectionItemView(save, disabled = disabled) {
|
||||
Text(stringResource(R.string.save_and_update_group_profile), color = if (disabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showUnsavedChangesAlert(save: () -> Unit, revert: () -> Unit) {
|
||||
AlertManager.shared.showAlertDialogStacked(
|
||||
title = generalGetString(R.string.save_welcome_message_question),
|
||||
confirmText = generalGetString(R.string.save_and_update_group_profile),
|
||||
dismissText = generalGetString(R.string.exit_without_saving),
|
||||
onConfirm = save,
|
||||
onDismiss = revert,
|
||||
)
|
||||
}
|
||||
@@ -6,14 +6,13 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
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.sp
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.model.*
|
||||
import chat.simplex.app.ui.theme.*
|
||||
import chat.simplex.res.MR
|
||||
|
||||
@Composable
|
||||
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
|
||||
.padding(horizontal = 4.dp)
|
||||
.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) {
|
||||
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 {
|
||||
AcceptCallButton(cInfo, acceptCall)
|
||||
}
|
||||
CICallStatus.Missed -> Icon(painterResource(MR.images.ic_call), stringResource(MR.strings.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.Missed -> Icon(painterResource(R.drawable.ic_call), stringResource(R.string.icon_descr_call_missed), 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.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 {
|
||||
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)
|
||||
}
|
||||
CICallStatus.Error -> {}
|
||||
@@ -53,9 +52,9 @@ fun CICallItemView(cInfo: ChatInfo, cItem: ChatItem, status: CICallStatus, durat
|
||||
@Composable
|
||||
fun AcceptCallButton(cInfo: ChatInfo, acceptCall: (Contact) -> Unit) {
|
||||
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 {
|
||||
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 {
|
||||
// Button {
|
||||
@@ -12,7 +12,6 @@ import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.model.*
|
||||
import chat.simplex.app.views.helpers.generalGetString
|
||||
import chat.simplex.res.MR
|
||||
|
||||
@Composable
|
||||
fun CIFeaturePreferenceView(
|
||||
@@ -31,7 +30,7 @@ fun CIFeaturePreferenceView(
|
||||
if (contact != null && allowed != FeatureAllowed.NO && contact.allowsFeature(feature) && !contact.userAllowsFeature(feature)) {
|
||||
val acceptStyle = SpanStyle(color = MaterialTheme.colors.primary, fontSize = 12.sp)
|
||||
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 annotatedText = buildAnnotatedString {
|
||||
withStyle(chatEventStyle) { append(chatItem.content.text + " ") }
|
||||
@@ -14,8 +14,8 @@ import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.*
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
@@ -23,7 +23,6 @@ import chat.simplex.app.R
|
||||
import chat.simplex.app.model.*
|
||||
import chat.simplex.app.ui.theme.*
|
||||
import chat.simplex.app.views.helpers.*
|
||||
import chat.simplex.res.MR
|
||||
import kotlinx.datetime.Clock
|
||||
|
||||
@Composable
|
||||
@@ -33,7 +32,7 @@ fun CIFileView(
|
||||
receiveFile: (Long) -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val saveFileLauncher = rememberSaveFileLauncher(ciFile = file)
|
||||
val saveFileLauncher = rememberSaveFileLauncher(cxt = context, ciFile = file)
|
||||
|
||||
@Composable
|
||||
fun fileIcon(
|
||||
@@ -44,15 +43,15 @@ fun CIFileView(
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_draft_filled),
|
||||
stringResource(MR.strings.icon_descr_file),
|
||||
painterResource(R.drawable.ic_draft_filled),
|
||||
stringResource(R.string.icon_descr_file),
|
||||
Modifier.fillMaxSize(),
|
||||
tint = color
|
||||
)
|
||||
if (innerIcon != null) {
|
||||
Icon(
|
||||
innerIcon,
|
||||
stringResource(MR.strings.icon_descr_file),
|
||||
stringResource(R.string.icon_descr_file),
|
||||
Modifier
|
||||
.size(32.dp)
|
||||
.padding(top = 12.dp),
|
||||
@@ -77,8 +76,8 @@ fun CIFileView(
|
||||
receiveFile(file.fileId)
|
||||
} else {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(MR.strings.large_file),
|
||||
String.format(generalGetString(MR.strings.contact_sent_large_file), formatBytes(getMaxFileSize(file.fileProtocol)))
|
||||
generalGetString(R.string.large_file),
|
||||
String.format(generalGetString(R.string.contact_sent_large_file), formatBytes(getMaxFileSize(file.fileProtocol)))
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -86,21 +85,21 @@ fun CIFileView(
|
||||
when (file.fileProtocol) {
|
||||
FileProtocol.XFTP ->
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(MR.strings.waiting_for_file),
|
||||
generalGetString(MR.strings.file_will_be_received_when_contact_completes_uploading)
|
||||
generalGetString(R.string.waiting_for_file),
|
||||
generalGetString(R.string.file_will_be_received_when_contact_completes_uploading)
|
||||
)
|
||||
FileProtocol.SMP ->
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(MR.strings.waiting_for_file),
|
||||
generalGetString(MR.strings.file_will_be_received_when_contact_is_online)
|
||||
generalGetString(R.string.waiting_for_file),
|
||||
generalGetString(R.string.file_will_be_received_when_contact_is_online)
|
||||
)
|
||||
}
|
||||
is CIFileStatus.RcvComplete -> {
|
||||
val filePath = getLoadedFilePath(file)
|
||||
val filePath = getLoadedFilePath(context, file)
|
||||
if (filePath != null) {
|
||||
saveFileLauncher.launch(file.fileName)
|
||||
} 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 -> {}
|
||||
@@ -152,15 +151,15 @@ fun CIFileView(
|
||||
FileProtocol.XFTP -> progressCircle(file.fileStatus.sndProgress, file.fileStatus.sndTotal)
|
||||
FileProtocol.SMP -> progressIndicator()
|
||||
}
|
||||
is CIFileStatus.SndComplete -> fileIcon(innerIcon = painterResource(MR.images.ic_check_filled))
|
||||
is CIFileStatus.SndCancelled -> fileIcon(innerIcon = painterResource(MR.images.ic_close))
|
||||
is CIFileStatus.SndError -> fileIcon(innerIcon = painterResource(MR.images.ic_close))
|
||||
is CIFileStatus.SndComplete -> fileIcon(innerIcon = painterResource(R.drawable.ic_check_filled))
|
||||
is CIFileStatus.SndCancelled -> fileIcon(innerIcon = painterResource(R.drawable.ic_close))
|
||||
is CIFileStatus.SndError -> fileIcon(innerIcon = painterResource(R.drawable.ic_close))
|
||||
is CIFileStatus.RcvInvitation ->
|
||||
if (fileSizeValid())
|
||||
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
|
||||
fileIcon(innerIcon = painterResource(MR.images.ic_priority_high), color = WarningOrange)
|
||||
is CIFileStatus.RcvAccepted -> fileIcon(innerIcon = painterResource(MR.images.ic_more_horiz))
|
||||
fileIcon(innerIcon = painterResource(R.drawable.ic_priority_high), color = WarningOrange)
|
||||
is CIFileStatus.RcvAccepted -> fileIcon(innerIcon = painterResource(R.drawable.ic_more_horiz))
|
||||
is CIFileStatus.RcvTransfer ->
|
||||
if (file.fileProtocol == FileProtocol.XFTP && file.fileStatus.rcvProgress < file.fileStatus.rcvTotal) {
|
||||
progressCircle(file.fileStatus.rcvProgress, file.fileStatus.rcvTotal)
|
||||
@@ -168,8 +167,8 @@ fun CIFileView(
|
||||
progressIndicator()
|
||||
}
|
||||
is CIFileStatus.RcvComplete -> fileIcon()
|
||||
is CIFileStatus.RcvCancelled -> fileIcon(innerIcon = painterResource(MR.images.ic_close))
|
||||
is CIFileStatus.RcvError -> fileIcon(innerIcon = painterResource(MR.images.ic_close))
|
||||
is CIFileStatus.RcvCancelled -> fileIcon(innerIcon = painterResource(R.drawable.ic_close))
|
||||
is CIFileStatus.RcvError -> fileIcon(innerIcon = painterResource(R.drawable.ic_close))
|
||||
}
|
||||
} else {
|
||||
fileIcon()
|
||||
@@ -212,7 +211,6 @@ class ChatItemProvider: PreviewParameterProvider<ChatItem> {
|
||||
meta = CIMeta.getSample(1, Clock.System.now(), "", CIStatus.SndSent(), itemEdited = true),
|
||||
content = CIContent.SndMsgContent(msgContent = MsgContent.MCFile("")),
|
||||
quotedItem = null,
|
||||
reactions = listOf(),
|
||||
file = CIFile.getSample(fileStatus = CIFileStatus.SndComplete)
|
||||
)
|
||||
private val fileChatItemWtFile = ChatItem(
|
||||
@@ -220,7 +218,6 @@ class ChatItemProvider: PreviewParameterProvider<ChatItem> {
|
||||
meta = CIMeta.getSample(1, Clock.System.now(), "", CIStatus.RcvRead(), ),
|
||||
content = CIContent.RcvMsgContent(msgContent = MsgContent.MCFile("")),
|
||||
quotedItem = null,
|
||||
reactions = listOf(),
|
||||
file = null
|
||||
)
|
||||
override val values = listOf(
|
||||
@@ -8,7 +8,7 @@ import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
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.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.*
|
||||
@@ -18,7 +18,6 @@ import chat.simplex.app.R
|
||||
import chat.simplex.app.model.*
|
||||
import chat.simplex.app.ui.theme.*
|
||||
import chat.simplex.app.views.helpers.*
|
||||
import chat.simplex.res.MR
|
||||
|
||||
@Composable
|
||||
fun CIGroupInvitationView(
|
||||
@@ -44,7 +43,7 @@ fun CIGroupInvitationView(
|
||||
.padding(vertical = 4.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))
|
||||
Column(
|
||||
Modifier.defaultMinSize(minHeight = 60.dp),
|
||||
@@ -61,11 +60,11 @@ fun CIGroupInvitationView(
|
||||
@Composable
|
||||
fun groupInvitationText() {
|
||||
when {
|
||||
sent -> Text(stringResource(MR.strings.you_sent_group_invitation))
|
||||
!sent && groupInvitation.status == CIGroupInvitationStatus.Pending -> Text(stringResource(MR.strings.you_are_invited_to_group))
|
||||
!sent && groupInvitation.status == CIGroupInvitationStatus.Accepted -> Text(stringResource(MR.strings.you_joined_this_group))
|
||||
!sent && groupInvitation.status == CIGroupInvitationStatus.Rejected -> Text(stringResource(MR.strings.you_rejected_group_invitation))
|
||||
!sent && groupInvitation.status == CIGroupInvitationStatus.Expired -> Text(stringResource(MR.strings.group_invitation_expired))
|
||||
sent -> Text(stringResource(R.string.you_sent_group_invitation))
|
||||
!sent && groupInvitation.status == CIGroupInvitationStatus.Pending -> Text(stringResource(R.string.you_are_invited_to_group))
|
||||
!sent && groupInvitation.status == CIGroupInvitationStatus.Accepted -> Text(stringResource(R.string.you_joined_this_group))
|
||||
!sent && groupInvitation.status == CIGroupInvitationStatus.Rejected -> Text(stringResource(R.string.you_rejected_group_invitation))
|
||||
!sent && groupInvitation.status == CIGroupInvitationStatus.Expired -> Text(stringResource(R.string.group_invitation_expired))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,7 +95,7 @@ fun CIGroupInvitationView(
|
||||
if (action) {
|
||||
groupInvitationText()
|
||||
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)
|
||||
} else {
|
||||
Box(Modifier.padding(end = 48.dp)) {
|
||||
@@ -2,6 +2,7 @@ package chat.simplex.app.views.chat.item
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.os.Build.VERSION.SDK_INT
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
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.layoutId
|
||||
import androidx.compose.ui.platform.*
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
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.core.content.FileProvider
|
||||
@@ -31,8 +32,6 @@ import coil.compose.rememberAsyncImagePainter
|
||||
import coil.decode.GifDecoder
|
||||
import coil.decode.ImageDecoderDecoder
|
||||
import coil.request.ImageRequest
|
||||
import chat.simplex.res.MR
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
import java.io.File
|
||||
|
||||
@Composable
|
||||
@@ -53,7 +52,7 @@ fun CIImageView(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun fileIcon(icon: Painter, stringId: StringResource) {
|
||||
fun fileIcon(icon: Painter, @StringRes stringId: Int) {
|
||||
Icon(
|
||||
icon,
|
||||
stringResource(stringId),
|
||||
@@ -78,14 +77,14 @@ fun CIImageView(
|
||||
FileProtocol.SMP -> {}
|
||||
}
|
||||
is CIFileStatus.SndTransfer -> progressIndicator()
|
||||
is CIFileStatus.SndComplete -> fileIcon(painterResource(MR.images.ic_check_filled), MR.strings.icon_descr_image_snd_complete)
|
||||
is CIFileStatus.SndCancelled -> fileIcon(painterResource(MR.images.ic_close), MR.strings.icon_descr_file)
|
||||
is CIFileStatus.SndError -> fileIcon(painterResource(MR.images.ic_close), MR.strings.icon_descr_file)
|
||||
is CIFileStatus.RcvInvitation -> fileIcon(painterResource(MR.images.ic_arrow_downward), MR.strings.icon_descr_asked_to_receive)
|
||||
is CIFileStatus.RcvAccepted -> fileIcon(painterResource(MR.images.ic_more_horiz), MR.strings.icon_descr_waiting_for_image)
|
||||
is CIFileStatus.SndComplete -> fileIcon(painterResource(R.drawable.ic_check_filled), R.string.icon_descr_image_snd_complete)
|
||||
is CIFileStatus.SndCancelled -> fileIcon(painterResource(R.drawable.ic_close), R.string.icon_descr_file)
|
||||
is CIFileStatus.SndError -> fileIcon(painterResource(R.drawable.ic_close), R.string.icon_descr_file)
|
||||
is CIFileStatus.RcvInvitation -> fileIcon(painterResource(R.drawable.ic_arrow_downward), R.string.icon_descr_asked_to_receive)
|
||||
is CIFileStatus.RcvAccepted -> fileIcon(painterResource(R.drawable.ic_more_horiz), R.string.icon_descr_waiting_for_image)
|
||||
is CIFileStatus.RcvTransfer -> progressIndicator()
|
||||
is CIFileStatus.RcvCancelled -> fileIcon(painterResource(MR.images.ic_close), MR.strings.icon_descr_file)
|
||||
is CIFileStatus.RcvError -> 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(R.drawable.ic_close), R.string.icon_descr_file)
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
@@ -102,7 +101,7 @@ fun CIImageView(
|
||||
fun imageView(imageBitmap: Bitmap, onClick: () -> Unit) {
|
||||
Image(
|
||||
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
|
||||
// if text is short and take all available width if text is long
|
||||
modifier = Modifier
|
||||
@@ -119,7 +118,7 @@ fun CIImageView(
|
||||
fun imageView(painter: Painter, onClick: () -> Unit) {
|
||||
Image(
|
||||
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
|
||||
// if text is short and take all available width if text is long
|
||||
modifier = Modifier
|
||||
@@ -140,8 +139,8 @@ fun CIImageView(
|
||||
}
|
||||
|
||||
fun imageAndFilePath(file: CIFile?): Pair<Bitmap?, String?> {
|
||||
val imageBitmap: Bitmap? = getLoadedImage(file)
|
||||
val filePath = getLoadedFilePath(file)
|
||||
val imageBitmap: Bitmap? = getLoadedImage(SimplexApp.context, file)
|
||||
val filePath = getLoadedFilePath(SimplexApp.context, file)
|
||||
return imageBitmap to filePath
|
||||
}
|
||||
|
||||
@@ -161,7 +160,7 @@ fun CIImageView(
|
||||
val view = LocalView.current
|
||||
imageView(imagePainter, onClick = {
|
||||
hideKeyboard(view)
|
||||
if (getLoadedFilePath(file) != null) {
|
||||
if (getLoadedFilePath(context, file) != null) {
|
||||
ModalManager.shared.showCustomModal(animated = false) { close ->
|
||||
ImageFullScreenView(imageProvider, close)
|
||||
}
|
||||
@@ -176,21 +175,21 @@ fun CIImageView(
|
||||
receiveFile(file.fileId)
|
||||
} else {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(MR.strings.large_file),
|
||||
String.format(generalGetString(MR.strings.contact_sent_large_file), formatBytes(getMaxFileSize(file.fileProtocol)))
|
||||
generalGetString(R.string.large_file),
|
||||
String.format(generalGetString(R.string.contact_sent_large_file), formatBytes(getMaxFileSize(file.fileProtocol)))
|
||||
)
|
||||
}
|
||||
CIFileStatus.RcvAccepted ->
|
||||
when (file.fileProtocol) {
|
||||
FileProtocol.XFTP ->
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(MR.strings.waiting_for_image),
|
||||
generalGetString(MR.strings.image_will_be_received_when_contact_completes_uploading)
|
||||
generalGetString(R.string.waiting_for_image),
|
||||
generalGetString(R.string.image_will_be_received_when_contact_completes_uploading)
|
||||
)
|
||||
FileProtocol.SMP ->
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(MR.strings.waiting_for_image),
|
||||
generalGetString(MR.strings.image_will_be_received_when_contact_is_online)
|
||||
generalGetString(R.string.waiting_for_image),
|
||||
generalGetString(R.string.image_will_be_received_when_contact_is_online)
|
||||
)
|
||||
}
|
||||
CIFileStatus.RcvTransfer(rcvProgress = 7, rcvTotal = 10) -> {} // ?
|
||||
@@ -1,5 +1,6 @@
|
||||
package chat.simplex.app.views.chat.item
|
||||
|
||||
import SectionSpacer
|
||||
import SectionView
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
@@ -7,15 +8,15 @@ import androidx.compose.material.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.ui.theme.DEFAULT_PADDING
|
||||
import chat.simplex.app.views.helpers.*
|
||||
import chat.simplex.app.views.usersettings.SettingsActionItem
|
||||
import chat.simplex.res.MR
|
||||
|
||||
@Composable
|
||||
fun CIInvalidJSONView(json: String) {
|
||||
@@ -23,7 +24,7 @@ fun CIInvalidJSONView(json: String) {
|
||||
.clickable { ModalManager.shared.showModal(true) { InvalidJSONView(json) } }
|
||||
.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 {
|
||||
Spacer(Modifier.height(DEFAULT_PADDING))
|
||||
SectionView {
|
||||
SettingsActionItem(painterResource(MR.images.ic_share), generalGetString(MR.strings.share_verb), click = {
|
||||
shareText(json)
|
||||
val context = LocalContext.current
|
||||
SettingsActionItem(painterResource(R.drawable.ic_share), generalGetString(R.string.share_verb), click = {
|
||||
shareText(context, json)
|
||||
})
|
||||
}
|
||||
Column(Modifier.padding(DEFAULT_PADDING).fillMaxWidth().verticalScroll(rememberScrollState())) {
|
||||
@@ -3,7 +3,8 @@ package chat.simplex.app.views.chat.item
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.*
|
||||
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.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -14,7 +15,6 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.app.model.*
|
||||
import chat.simplex.app.ui.theme.CurrentColors
|
||||
import chat.simplex.res.MR
|
||||
import kotlinx.datetime.Clock
|
||||
|
||||
@Composable
|
||||
@@ -24,7 +24,7 @@ fun CIMetaView(chatItem: ChatItem, timedMessagesTTL: Int?, metaColor: Color = Ma
|
||||
Text(
|
||||
chatItem.timestampText,
|
||||
color = metaColor,
|
||||
fontSize = 12.sp,
|
||||
fontSize = 14.sp,
|
||||
modifier = Modifier.padding(start = 3.dp)
|
||||
)
|
||||
} else {
|
||||
@@ -37,14 +37,14 @@ fun CIMetaView(chatItem: ChatItem, timedMessagesTTL: Int?, metaColor: Color = Ma
|
||||
// changing this function requires updating reserveSpaceForMeta
|
||||
private fun CIMetaText(meta: CIMeta, chatTTL: Int?, color: Color) {
|
||||
if (meta.itemEdited) {
|
||||
StatusIconText(painterResource(MR.images.ic_edit), color)
|
||||
StatusIconText(painterResource(R.drawable.ic_edit), color)
|
||||
Spacer(Modifier.width(3.dp))
|
||||
}
|
||||
if (meta.disappearing) {
|
||||
StatusIconText(painterResource(MR.images.ic_timer), color)
|
||||
StatusIconText(painterResource(R.drawable.ic_timer), color)
|
||||
val ttl = meta.itemTimed?.ttl
|
||||
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))
|
||||
}
|
||||
@@ -54,10 +54,10 @@ private fun CIMetaText(meta: CIMeta, chatTTL: Int?, color: Color) {
|
||||
StatusIconText(painterResource(icon), statusColor)
|
||||
Spacer(Modifier.width(4.dp))
|
||||
} 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))
|
||||
}
|
||||
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
|
||||
@@ -69,7 +69,7 @@ fun reserveSpaceForMeta(meta: CIMeta, chatTTL: Int?): String {
|
||||
res += iconSpace
|
||||
val ttl = meta.itemTimed.ttl
|
||||
if (ttl != chatTTL) {
|
||||
res += shortTimeText(ttl)
|
||||
res += TimedMessagesPreference.shortTtlText(ttl)
|
||||
}
|
||||
}
|
||||
if (meta.statusIcon(CurrentColors.value.colors.secondary) != null || !meta.disappearing) {
|
||||
@@ -0,0 +1,24 @@
|
||||
package chat.simplex.app.views.chat.item
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.model.*
|
||||
import chat.simplex.app.views.helpers.AlertManager
|
||||
import chat.simplex.app.views.helpers.generalGetString
|
||||
|
||||
@Composable
|
||||
fun CIRcvDecryptionError(msgDecryptError: MsgDecryptError, msgCount: UInt, ci: ChatItem, timedMessagesTTL: Int?, showMember: Boolean) {
|
||||
CIMsgError(ci, timedMessagesTTL, showMember) {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(R.string.decryption_error),
|
||||
text = when (msgDecryptError) {
|
||||
MsgDecryptError.RatchetHeader -> String.format(generalGetString(R.string.alert_text_decryption_error_header), msgCount.toLong()) + "\n" +
|
||||
generalGetString(R.string.alert_text_fragment_encryption_out_of_sync_old_database) + "\n" +
|
||||
generalGetString(R.string.alert_text_fragment_permanent_error_reconnect)
|
||||
MsgDecryptError.TooManySkipped -> String.format(generalGetString(R.string.alert_text_decryption_error_too_many_skipped), msgCount.toLong()) + "\n" +
|
||||
generalGetString(R.string.alert_text_fragment_encryption_out_of_sync_old_database) + "\n" +
|
||||
generalGetString(R.string.alert_text_fragment_permanent_error_reconnect)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package chat.simplex.app.views.chat.item
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Rect
|
||||
import android.net.Uri
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
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.layout.*
|
||||
import androidx.compose.ui.platform.*
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.*
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.core.content.FileProvider
|
||||
@@ -29,8 +30,6 @@ import chat.simplex.app.ui.theme.*
|
||||
import chat.simplex.app.views.helpers.*
|
||||
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout.RESIZE_MODE_FIXED_WIDTH
|
||||
import com.google.android.exoplayer2.ui.StyledPlayerView
|
||||
import chat.simplex.res.MR
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
import java.io.File
|
||||
|
||||
@Composable
|
||||
@@ -47,7 +46,7 @@ fun CIVideoView(
|
||||
contentAlignment = Alignment.TopEnd
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val filePath = remember(file) { getLoadedFilePath(file) }
|
||||
val filePath = remember(file) { getLoadedFilePath(SimplexApp.context, file) }
|
||||
val preview = remember(image) { base64ToBitmap(image) }
|
||||
if (file != null && filePath != null) {
|
||||
val uri = remember(filePath) { FileProvider.getUriForFile(context, "${BuildConfig.APPLICATION_ID}.provider", File(filePath)) }
|
||||
@@ -69,14 +68,14 @@ fun CIVideoView(
|
||||
when (file.fileProtocol) {
|
||||
FileProtocol.XFTP ->
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(MR.strings.waiting_for_video),
|
||||
generalGetString(MR.strings.video_will_be_received_when_contact_completes_uploading)
|
||||
generalGetString(R.string.waiting_for_video),
|
||||
generalGetString(R.string.video_will_be_received_when_contact_completes_uploading)
|
||||
)
|
||||
|
||||
FileProtocol.SMP ->
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(MR.strings.waiting_for_video),
|
||||
generalGetString(MR.strings.video_will_be_received_when_contact_is_online)
|
||||
generalGetString(R.string.waiting_for_video),
|
||||
generalGetString(R.string.video_will_be_received_when_contact_is_online)
|
||||
)
|
||||
}
|
||||
CIFileStatus.RcvTransfer(rcvProgress = 7, rcvTotal = 10) -> {} // ?
|
||||
@@ -101,7 +100,7 @@ fun CIVideoView(
|
||||
@Composable
|
||||
private fun VideoView(uri: Uri, file: CIFile, defaultPreview: Bitmap, defaultDuration: Long, showMenu: MutableState<Boolean>, onClick: () -> Unit) {
|
||||
val context = LocalContext.current
|
||||
val player = remember(uri) { VideoPlayer.getOrCreate(uri, false, defaultPreview, defaultDuration, true) }
|
||||
val player = remember(uri) { VideoPlayer.getOrCreate(uri, false, defaultPreview, defaultDuration, true, context) }
|
||||
val videoPlaying = remember(uri.path) { player.videoPlaying }
|
||||
val progress = remember(uri.path) { player.progress }
|
||||
val duration = remember(uri.path) { player.duration }
|
||||
@@ -162,7 +161,7 @@ private fun BoxScope.PlayButton(error: Boolean = false, onLongClick: () -> Unit,
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_play_arrow_filled),
|
||||
painterResource(R.drawable.ic_play_arrow_filled),
|
||||
contentDescription = null,
|
||||
tint = if (error) WarningOrange else Color.White
|
||||
)
|
||||
@@ -190,7 +189,7 @@ private fun DurationProgress(file: CIFile, playing: MutableState<Boolean>, durat
|
||||
color = Color.White
|
||||
)
|
||||
/*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),
|
||||
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 }
|
||||
Image(
|
||||
preview.asImageBitmap(),
|
||||
contentDescription = stringResource(MR.strings.video_descr),
|
||||
contentDescription = stringResource(R.string.video_descr),
|
||||
modifier = Modifier
|
||||
.width(width)
|
||||
.combinedClickable(
|
||||
@@ -253,7 +252,7 @@ private fun progressIndicator() {
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun fileIcon(icon: Painter, stringId: StringResource) {
|
||||
private fun fileIcon(icon: Painter, @StringRes stringId: Int) {
|
||||
Icon(
|
||||
icon,
|
||||
stringResource(stringId),
|
||||
@@ -296,19 +295,19 @@ private fun loadingIndicator(file: CIFile?) {
|
||||
FileProtocol.XFTP -> progressCircle(file.fileStatus.sndProgress, file.fileStatus.sndTotal)
|
||||
FileProtocol.SMP -> progressIndicator()
|
||||
}
|
||||
is CIFileStatus.SndComplete -> fileIcon(painterResource(MR.images.ic_check_filled), MR.strings.icon_descr_video_snd_complete)
|
||||
is CIFileStatus.SndCancelled -> fileIcon(painterResource(MR.images.ic_close), MR.strings.icon_descr_file)
|
||||
is CIFileStatus.SndError -> fileIcon(painterResource(MR.images.ic_close), MR.strings.icon_descr_file)
|
||||
is CIFileStatus.RcvInvitation -> fileIcon(painterResource(MR.images.ic_arrow_downward), MR.strings.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.SndComplete -> fileIcon(painterResource(R.drawable.ic_check_filled), R.string.icon_descr_video_snd_complete)
|
||||
is CIFileStatus.SndCancelled -> fileIcon(painterResource(R.drawable.ic_close), R.string.icon_descr_file)
|
||||
is CIFileStatus.SndError -> fileIcon(painterResource(R.drawable.ic_close), R.string.icon_descr_file)
|
||||
is CIFileStatus.RcvInvitation -> fileIcon(painterResource(R.drawable.ic_arrow_downward), R.string.icon_descr_video_asked_to_receive)
|
||||
is CIFileStatus.RcvAccepted -> fileIcon(painterResource(R.drawable.ic_more_horiz), R.string.icon_descr_waiting_for_video)
|
||||
is CIFileStatus.RcvTransfer ->
|
||||
if (file.fileProtocol == FileProtocol.XFTP && file.fileStatus.rcvProgress < file.fileStatus.rcvTotal) {
|
||||
progressCircle(file.fileStatus.rcvProgress, file.fileStatus.rcvTotal)
|
||||
} else {
|
||||
progressIndicator()
|
||||
}
|
||||
is CIFileStatus.RcvCancelled -> fileIcon(painterResource(MR.images.ic_close), MR.strings.icon_descr_file)
|
||||
is CIFileStatus.RcvError -> 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(R.drawable.ic_close), R.string.icon_descr_file)
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
@@ -327,8 +326,8 @@ private fun receiveFileIfValidSize(file: CIFile, receiveFile: (Long) -> Unit) {
|
||||
receiveFile(file.fileId)
|
||||
} else {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(MR.strings.large_file),
|
||||
String.format(generalGetString(MR.strings.contact_sent_large_file), formatBytes(getMaxFileSize(file.fileProtocol)))
|
||||
generalGetString(R.string.large_file),
|
||||
String.format(generalGetString(R.string.contact_sent_large_file), formatBytes(getMaxFileSize(file.fileProtocol)))
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -9,19 +9,20 @@ import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
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.Size
|
||||
import androidx.compose.ui.graphics.*
|
||||
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 chat.simplex.app.R
|
||||
import chat.simplex.app.model.*
|
||||
import chat.simplex.app.ui.theme.*
|
||||
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
|
||||
|
||||
@@ -35,15 +36,14 @@ fun CIVoiceView(
|
||||
ci: ChatItem,
|
||||
timedMessagesTTL: Int?,
|
||||
longClick: () -> Unit,
|
||||
receiveFile: (Long) -> Unit,
|
||||
) {
|
||||
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
|
||||
) {
|
||||
if (file != null) {
|
||||
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) }
|
||||
val audioPlaying = rememberSaveable(file.filePath) { mutableStateOf(false) }
|
||||
val progress = rememberSaveable(file.filePath) { mutableStateOf(0) }
|
||||
@@ -64,11 +64,9 @@ fun CIVoiceView(
|
||||
durationText(time / 1000)
|
||||
}
|
||||
}
|
||||
VoiceLayout(file, ci, text, audioPlaying, progress, duration, brokenAudio, sent, hasText, timedMessagesTTL, play, pause, longClick, receiveFile) {
|
||||
AudioPlayer.seekTo(it, progress, filePath)
|
||||
}
|
||||
VoiceLayout(file, ci, text, audioPlaying, progress, duration, brokenAudio, sent, hasText, timedMessagesTTL, play, pause, longClick)
|
||||
} else {
|
||||
VoiceMsgIndicator(null, false, sent, hasText, null, null, false, {}, {}, longClick, receiveFile)
|
||||
VoiceMsgIndicator(null, false, sent, hasText, null, null, false, {}, {}, longClick)
|
||||
val metaReserve = if (edited)
|
||||
" "
|
||||
else
|
||||
@@ -92,89 +90,39 @@ private fun VoiceLayout(
|
||||
timedMessagesTTL: Int?,
|
||||
play: () -> Unit,
|
||||
pause: () -> Unit,
|
||||
longClick: () -> Unit,
|
||||
receiveFile: (Long) -> Unit,
|
||||
onProgressChanged: (Int) -> Unit,
|
||||
longClick: () -> 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 {
|
||||
hasText -> {
|
||||
val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage
|
||||
val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage
|
||||
Spacer(Modifier.width(6.dp))
|
||||
VoiceMsgIndicator(file, audioPlaying.value, sent, hasText, progress, duration, brokenAudio, play, pause, longClick, receiveFile)
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
DurationText(text, PaddingValues(start = 12.dp))
|
||||
Slider(if (ci.chatDir.sent) sentColor else receivedColor)
|
||||
}
|
||||
VoiceMsgIndicator(file, audioPlaying.value, sent, hasText, progress, duration, brokenAudio, play, pause, longClick)
|
||||
DurationText(text, PaddingValues(start = 12.dp))
|
||||
}
|
||||
sent -> {
|
||||
Column(horizontalAlignment = Alignment.End) {
|
||||
Row {
|
||||
Row(Modifier.weight(1f, false), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.End) {
|
||||
Spacer(Modifier.height(56.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)
|
||||
Row {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Spacer(Modifier.height(56.dp))
|
||||
DurationText(text, PaddingValues(end = 12.dp))
|
||||
}
|
||||
Box(Modifier.padding(top = 6.dp, end = 6.dp)) {
|
||||
CIMetaView(ci, timedMessagesTTL)
|
||||
Column {
|
||||
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 -> {
|
||||
Column(horizontalAlignment = Alignment.Start) {
|
||||
Row {
|
||||
VoiceMsgIndicator(file, audioPlaying.value, sent, hasText, progress, duration, brokenAudio, play, pause, longClick, receiveFile)
|
||||
Row(Modifier.weight(1f, false), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Start) {
|
||||
DurationText(text, PaddingValues(start = 12.dp))
|
||||
Slider(MaterialTheme.colors.background, PaddingValues(start = DEFAULT_PADDING_HALF + 3.dp))
|
||||
Spacer(Modifier.height(56.dp))
|
||||
Row {
|
||||
Column {
|
||||
VoiceMsgIndicator(file, audioPlaying.value, sent, hasText, progress, duration, brokenAudio, play, pause, longClick)
|
||||
Box(Modifier.align(Alignment.CenterHorizontally).padding(top = 6.dp)) {
|
||||
CIMetaView(ci, timedMessagesTTL)
|
||||
}
|
||||
}
|
||||
Box(Modifier.padding(top = 6.dp)) {
|
||||
CIMetaView(ci, timedMessagesTTL)
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
DurationText(text, PaddingValues(start = 12.dp))
|
||||
Spacer(Modifier.height(56.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -225,7 +173,7 @@ private fun PlayPauseButton(
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
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,
|
||||
Modifier.size(36.dp),
|
||||
tint = if (error) WarningOrange else if (!enabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary
|
||||
@@ -245,8 +193,7 @@ private fun VoiceMsgIndicator(
|
||||
error: Boolean,
|
||||
play: () -> Unit,
|
||||
pause: () -> Unit,
|
||||
longClick: () -> Unit,
|
||||
receiveFile: (Long) -> Unit,
|
||||
longClick: () -> Unit
|
||||
) {
|
||||
val strokeWidth = with(LocalDensity.current) { 3.dp.toPx() }
|
||||
val strokeColor = MaterialTheme.colors.primary
|
||||
@@ -255,7 +202,7 @@ private fun VoiceMsgIndicator(
|
||||
if (hasText) {
|
||||
IconButton({ if (!audioPlaying) play() else pause() }, Modifier.size(56.dp).drawRingModifier(angle, strokeColor, strokeWidth)) {
|
||||
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,
|
||||
Modifier.size(36.dp),
|
||||
tint = MaterialTheme.colors.primary
|
||||
@@ -265,9 +212,8 @@ private fun VoiceMsgIndicator(
|
||||
PlayPauseButton(audioPlaying, sent, angle, strokeWidth, strokeColor, true, error, play, pause, longClick = longClick)
|
||||
}
|
||||
} else {
|
||||
if (file?.fileStatus is CIFileStatus.RcvInvitation) {
|
||||
PlayPauseButton(audioPlaying, sent, 0f, strokeWidth, strokeColor, true, error, { receiveFile(file.fileId) }, {}, longClick = longClick)
|
||||
} else if (file?.fileStatus is CIFileStatus.RcvTransfer
|
||||
if (file?.fileStatus is CIFileStatus.RcvInvitation
|
||||
|| file?.fileStatus is CIFileStatus.RcvTransfer
|
||||
|| file?.fileStatus is CIFileStatus.RcvAccepted
|
||||
) {
|
||||
Box(
|
||||
@@ -0,0 +1,457 @@
|
||||
package chat.simplex.app.views.chat.item
|
||||
|
||||
import android.Manifest
|
||||
import android.os.Build
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.*
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.app.*
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.model.*
|
||||
import chat.simplex.app.ui.theme.*
|
||||
import chat.simplex.app.views.chat.ComposeContextItem
|
||||
import chat.simplex.app.views.chat.ComposeState
|
||||
import chat.simplex.app.views.helpers.*
|
||||
import com.google.accompanist.permissions.rememberPermissionState
|
||||
import kotlinx.datetime.Clock
|
||||
|
||||
// TODO refactor so that FramedItemView can show all CIContent items if they're deleted (see Swift code)
|
||||
|
||||
@Composable
|
||||
fun ChatItemView(
|
||||
cInfo: ChatInfo,
|
||||
cItem: ChatItem,
|
||||
composeState: MutableState<ComposeState>,
|
||||
imageProvider: (() -> ImageGalleryProvider)? = null,
|
||||
showMember: Boolean = false,
|
||||
useLinkPreviews: Boolean,
|
||||
linkMode: SimplexLinkMode,
|
||||
deleteMessage: (Long, CIDeleteMode) -> Unit,
|
||||
receiveFile: (Long) -> Unit,
|
||||
cancelFile: (Long) -> Unit,
|
||||
joinGroup: (Long) -> Unit,
|
||||
acceptCall: (Contact) -> Unit,
|
||||
scrollToItem: (Long) -> Unit,
|
||||
acceptFeature: (Contact, ChatFeature, Int?) -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val uriHandler = LocalUriHandler.current
|
||||
val sent = cItem.chatDir.sent
|
||||
val alignment = if (sent) Alignment.CenterEnd else Alignment.CenterStart
|
||||
val showMenu = remember { mutableStateOf(false) }
|
||||
val revealed = remember { mutableStateOf(false) }
|
||||
val fullDeleteAllowed = remember(cInfo) { cInfo.featureEnabled(ChatFeature.FullDelete) }
|
||||
val saveFileLauncher = rememberSaveFileLauncher(cxt = context, ciFile = cItem.file)
|
||||
val onLinkLongClick = { _: String -> showMenu.value = true }
|
||||
val live = composeState.value.liveMessage != null
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(bottom = 4.dp)
|
||||
.fillMaxWidth(),
|
||||
contentAlignment = alignment,
|
||||
) {
|
||||
val onClick = {
|
||||
when (cItem.meta.itemStatus) {
|
||||
is CIStatus.SndErrorAuth -> {
|
||||
showMsgDeliveryErrorAlert(generalGetString(R.string.message_delivery_error_desc))
|
||||
}
|
||||
is CIStatus.SndError -> {
|
||||
showMsgDeliveryErrorAlert(generalGetString(R.string.unknown_error) + ": ${cItem.meta.itemStatus.agentError}")
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
Column(
|
||||
Modifier
|
||||
.clip(RoundedCornerShape(18.dp))
|
||||
.combinedClickable(onLongClick = { showMenu.value = true }, onClick = onClick),
|
||||
) {
|
||||
@Composable
|
||||
fun framedItemView() {
|
||||
FramedItemView(cInfo, cItem, uriHandler, imageProvider, showMember = showMember, linkMode = linkMode, showMenu, receiveFile, onLinkLongClick, scrollToItem)
|
||||
}
|
||||
|
||||
fun deleteMessageQuestionText(): String {
|
||||
return if (fullDeleteAllowed) {
|
||||
generalGetString(R.string.delete_message_cannot_be_undone_warning)
|
||||
} else {
|
||||
generalGetString(R.string.delete_message_mark_deleted_warning)
|
||||
}
|
||||
}
|
||||
|
||||
fun moderateMessageQuestionText(): String {
|
||||
return if (fullDeleteAllowed) {
|
||||
generalGetString(R.string.moderate_message_will_be_deleted_warning)
|
||||
} else {
|
||||
generalGetString(R.string.moderate_message_will_be_marked_warning)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MsgContentItemDropdownMenu() {
|
||||
DefaultDropdownMenu(showMenu) {
|
||||
if (cItem.meta.itemDeleted == null && !live) {
|
||||
ItemAction(stringResource(R.string.reply_verb), painterResource(R.drawable.ic_reply), onClick = {
|
||||
if (composeState.value.editing) {
|
||||
composeState.value = ComposeState(contextItem = ComposeContextItem.QuotedItem(cItem), useLinkPreviews = useLinkPreviews)
|
||||
} else {
|
||||
composeState.value = composeState.value.copy(contextItem = ComposeContextItem.QuotedItem(cItem))
|
||||
}
|
||||
showMenu.value = false
|
||||
})
|
||||
}
|
||||
ItemAction(stringResource(R.string.share_verb), painterResource(R.drawable.ic_share), onClick = {
|
||||
val filePath = getLoadedFilePath(SimplexApp.context, cItem.file)
|
||||
when {
|
||||
filePath != null -> shareFile(context, cItem.text, filePath)
|
||||
else -> shareText(context, cItem.content.text)
|
||||
}
|
||||
showMenu.value = false
|
||||
})
|
||||
ItemAction(stringResource(R.string.copy_verb), painterResource(R.drawable.ic_content_copy), onClick = {
|
||||
copyText(context, cItem.content.text)
|
||||
showMenu.value = false
|
||||
})
|
||||
if (cItem.content.msgContent is MsgContent.MCImage || cItem.content.msgContent is MsgContent.MCVideo || cItem.content.msgContent is MsgContent.MCFile || cItem.content.msgContent is MsgContent.MCVoice) {
|
||||
val filePath = getLoadedFilePath(context, cItem.file)
|
||||
if (filePath != null) {
|
||||
val writePermissionState = rememberPermissionState(permission = Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
ItemAction(stringResource(R.string.save_verb), painterResource(R.drawable.ic_download), onClick = {
|
||||
when (cItem.content.msgContent) {
|
||||
is MsgContent.MCImage -> {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R || writePermissionState.hasPermission) {
|
||||
saveImage(context, cItem.file)
|
||||
} else {
|
||||
writePermissionState.launchPermissionRequest()
|
||||
}
|
||||
}
|
||||
is MsgContent.MCFile, is MsgContent.MCVoice, is MsgContent.MCVideo -> saveFileLauncher.launch(cItem.file?.fileName)
|
||||
else -> {}
|
||||
}
|
||||
showMenu.value = false
|
||||
})
|
||||
}
|
||||
}
|
||||
if (cItem.meta.editable && cItem.content.msgContent !is MsgContent.MCVoice && !live) {
|
||||
ItemAction(stringResource(R.string.edit_verb), painterResource(R.drawable.ic_edit_filled), onClick = {
|
||||
composeState.value = ComposeState(editingItem = cItem, useLinkPreviews = useLinkPreviews)
|
||||
showMenu.value = false
|
||||
})
|
||||
}
|
||||
if (cItem.meta.itemDeleted != null && revealed.value) {
|
||||
ItemAction(
|
||||
stringResource(R.string.hide_verb),
|
||||
painterResource(R.drawable.ic_visibility_off),
|
||||
onClick = {
|
||||
revealed.value = false
|
||||
showMenu.value = false
|
||||
}
|
||||
)
|
||||
}
|
||||
if (cItem.meta.itemDeleted == null && cItem.file != null && cItem.file.cancelAction != null) {
|
||||
CancelFileItemAction(cItem.file.fileId, showMenu, cancelFile = cancelFile, cancelAction = cItem.file.cancelAction)
|
||||
}
|
||||
if (!(live && cItem.meta.isLive)) {
|
||||
DeleteItemAction(cItem, showMenu, questionText = deleteMessageQuestionText(), deleteMessage)
|
||||
}
|
||||
val groupInfo = cItem.memberToModerate(cInfo)?.first
|
||||
if (groupInfo != null) {
|
||||
ModerateItemAction(cItem, questionText = moderateMessageQuestionText(), showMenu, deleteMessage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MarkedDeletedItemDropdownMenu() {
|
||||
DefaultDropdownMenu(showMenu) {
|
||||
if (!cItem.isDeletedContent) {
|
||||
ItemAction(
|
||||
stringResource(R.string.reveal_verb),
|
||||
painterResource(R.drawable.ic_visibility),
|
||||
onClick = {
|
||||
revealed.value = true
|
||||
showMenu.value = false
|
||||
}
|
||||
)
|
||||
}
|
||||
DeleteItemAction(cItem, showMenu, questionText = deleteMessageQuestionText(), deleteMessage)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ContentItem() {
|
||||
val mc = cItem.content.msgContent
|
||||
if (cItem.meta.itemDeleted != null && !revealed.value) {
|
||||
MarkedDeletedItemView(cItem, cInfo.timedMessagesTTL, showMember = showMember)
|
||||
MarkedDeletedItemDropdownMenu()
|
||||
} else if (cItem.quotedItem == null && cItem.meta.itemDeleted == null && !cItem.meta.isLive) {
|
||||
if (mc is MsgContent.MCText && isShortEmoji(cItem.content.text)) {
|
||||
EmojiItemView(cItem, cInfo.timedMessagesTTL)
|
||||
MsgContentItemDropdownMenu()
|
||||
} else if (mc is MsgContent.MCVoice && cItem.content.text.isEmpty()) {
|
||||
CIVoiceView(mc.duration, cItem.file, cItem.meta.itemEdited, cItem.chatDir.sent, hasText = false, cItem, cInfo.timedMessagesTTL, longClick = { onLinkLongClick("") })
|
||||
MsgContentItemDropdownMenu()
|
||||
} else {
|
||||
framedItemView()
|
||||
MsgContentItemDropdownMenu()
|
||||
}
|
||||
} else {
|
||||
framedItemView()
|
||||
MsgContentItemDropdownMenu()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable fun DeletedItem() {
|
||||
DeletedItemView(cItem, cInfo.timedMessagesTTL, showMember = showMember)
|
||||
DefaultDropdownMenu(showMenu) {
|
||||
DeleteItemAction(cItem, showMenu, questionText = deleteMessageQuestionText(), deleteMessage)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable fun CallItem(status: CICallStatus, duration: Int) {
|
||||
CICallItemView(cInfo, cItem, status, duration, acceptCall)
|
||||
}
|
||||
|
||||
when (val c = cItem.content) {
|
||||
is CIContent.SndMsgContent -> ContentItem()
|
||||
is CIContent.RcvMsgContent -> ContentItem()
|
||||
is CIContent.SndDeleted -> DeletedItem()
|
||||
is CIContent.RcvDeleted -> DeletedItem()
|
||||
is CIContent.SndCall -> CallItem(c.status, c.duration)
|
||||
is CIContent.RcvCall -> CallItem(c.status, c.duration)
|
||||
is CIContent.RcvIntegrityError -> IntegrityErrorItemView(c.msgError, cItem, cInfo.timedMessagesTTL, showMember = showMember)
|
||||
is CIContent.RcvDecryptionError -> CIRcvDecryptionError(c.msgDecryptError, c.msgCount, cItem, cInfo.timedMessagesTTL, showMember = showMember)
|
||||
is CIContent.RcvGroupInvitation -> CIGroupInvitationView(cItem, c.groupInvitation, c.memberRole, joinGroup = joinGroup, chatIncognito = cInfo.incognito)
|
||||
is CIContent.SndGroupInvitation -> CIGroupInvitationView(cItem, c.groupInvitation, c.memberRole, joinGroup = joinGroup, chatIncognito = cInfo.incognito)
|
||||
is CIContent.RcvGroupEventContent -> CIEventView(cItem)
|
||||
is CIContent.SndGroupEventContent -> CIEventView(cItem)
|
||||
is CIContent.RcvConnEventContent -> CIEventView(cItem)
|
||||
is CIContent.SndConnEventContent -> CIEventView(cItem)
|
||||
is CIContent.RcvChatFeature -> CIChatFeatureView(cItem, c.feature, c.enabled.iconColor)
|
||||
is CIContent.SndChatFeature -> CIChatFeatureView(cItem, c.feature, c.enabled.iconColor)
|
||||
is CIContent.RcvChatPreference -> {
|
||||
val ct = if (cInfo is ChatInfo.Direct) cInfo.contact else null
|
||||
CIFeaturePreferenceView(cItem, ct, c.feature, c.allowed, acceptFeature)
|
||||
}
|
||||
is CIContent.SndChatPreference -> CIChatFeatureView(cItem, c.feature, MaterialTheme.colors.secondary, icon = c.feature.icon,)
|
||||
is CIContent.RcvGroupFeature -> CIChatFeatureView(cItem, c.groupFeature, c.preference.enable.iconColor)
|
||||
is CIContent.SndGroupFeature -> CIChatFeatureView(cItem, c.groupFeature, c.preference.enable.iconColor)
|
||||
is CIContent.RcvChatFeatureRejected -> CIChatFeatureView(cItem, c.feature, Color.Red)
|
||||
is CIContent.RcvGroupFeatureRejected -> CIChatFeatureView(cItem, c.groupFeature, Color.Red)
|
||||
is CIContent.SndModerated -> MarkedDeletedItemView(cItem, cInfo.timedMessagesTTL, showMember = showMember)
|
||||
is CIContent.RcvModerated -> MarkedDeletedItemView(cItem, cInfo.timedMessagesTTL, showMember = showMember)
|
||||
is CIContent.InvalidJSON -> CIInvalidJSONView(c.json)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CancelFileItemAction(
|
||||
fileId: Long,
|
||||
showMenu: MutableState<Boolean>,
|
||||
cancelFile: (Long) -> Unit,
|
||||
cancelAction: CancelAction
|
||||
) {
|
||||
ItemAction(
|
||||
stringResource(cancelAction.uiActionId),
|
||||
painterResource(R.drawable.ic_close),
|
||||
onClick = {
|
||||
showMenu.value = false
|
||||
cancelFileAlertDialog(fileId, cancelFile = cancelFile, cancelAction = cancelAction)
|
||||
},
|
||||
color = Color.Red
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DeleteItemAction(
|
||||
cItem: ChatItem,
|
||||
showMenu: MutableState<Boolean>,
|
||||
questionText: String,
|
||||
deleteMessage: (Long, CIDeleteMode) -> Unit
|
||||
) {
|
||||
ItemAction(
|
||||
stringResource(R.string.delete_verb),
|
||||
painterResource(R.drawable.ic_delete),
|
||||
onClick = {
|
||||
showMenu.value = false
|
||||
deleteMessageAlertDialog(cItem, questionText, deleteMessage = deleteMessage)
|
||||
},
|
||||
color = Color.Red
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ModerateItemAction(
|
||||
cItem: ChatItem,
|
||||
questionText: String,
|
||||
showMenu: MutableState<Boolean>,
|
||||
deleteMessage: (Long, CIDeleteMode) -> Unit
|
||||
) {
|
||||
ItemAction(
|
||||
stringResource(R.string.moderate_verb),
|
||||
painterResource(R.drawable.ic_flag),
|
||||
onClick = {
|
||||
showMenu.value = false
|
||||
moderateMessageAlertDialog(cItem, questionText, deleteMessage = deleteMessage)
|
||||
},
|
||||
color = Color.Red
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ItemAction(text: String, icon: Painter, onClick: () -> Unit, color: Color = Color.Unspecified) {
|
||||
val finalColor = if (color == Color.Unspecified) {
|
||||
if (isInDarkTheme()) MenuTextColorDark else Color.Black
|
||||
} else color
|
||||
DropdownMenuItem(onClick, contentPadding = PaddingValues(horizontal = DEFAULT_PADDING * 1.5f)) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(
|
||||
text,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1F)
|
||||
.padding(end = 15.dp),
|
||||
color = finalColor
|
||||
)
|
||||
Icon(icon, text, tint = finalColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ItemAction(text: String, icon: ImageVector, onClick: () -> Unit, color: Color = Color.Unspecified) {
|
||||
val finalColor = if (color == Color.Unspecified) {
|
||||
if (isInDarkTheme()) MenuTextColorDark else Color.Black
|
||||
} else color
|
||||
DropdownMenuItem(onClick, contentPadding = PaddingValues(horizontal = DEFAULT_PADDING * 1.5f)) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(
|
||||
text,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1F)
|
||||
.padding(end = 15.dp),
|
||||
color = finalColor
|
||||
)
|
||||
Icon(icon, text, tint = finalColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun cancelFileAlertDialog(fileId: Long, cancelFile: (Long) -> Unit, cancelAction: CancelAction) {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(cancelAction.alert.titleId),
|
||||
text = generalGetString(cancelAction.alert.messageId),
|
||||
confirmText = generalGetString(cancelAction.alert.confirmId),
|
||||
destructive = true,
|
||||
onConfirm = {
|
||||
cancelFile(fileId)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fun deleteMessageAlertDialog(chatItem: ChatItem, questionText: String, deleteMessage: (Long, CIDeleteMode) -> Unit) {
|
||||
AlertManager.shared.showAlertDialogButtons(
|
||||
title = generalGetString(R.string.delete_message__question),
|
||||
text = questionText,
|
||||
buttons = {
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 8.dp, vertical = 2.dp),
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
) {
|
||||
TextButton(onClick = {
|
||||
deleteMessage(chatItem.id, CIDeleteMode.cidmInternal)
|
||||
AlertManager.shared.hideAlert()
|
||||
}) { Text(stringResource(R.string.for_me_only), color = MaterialTheme.colors.error) }
|
||||
if (chatItem.meta.editable) {
|
||||
Spacer(Modifier.padding(horizontal = 4.dp))
|
||||
TextButton(onClick = {
|
||||
deleteMessage(chatItem.id, CIDeleteMode.cidmBroadcast)
|
||||
AlertManager.shared.hideAlert()
|
||||
}) { Text(stringResource(R.string.for_everybody), color = MaterialTheme.colors.error) }
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fun moderateMessageAlertDialog(chatItem: ChatItem, questionText: String, deleteMessage: (Long, CIDeleteMode) -> Unit) {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(R.string.delete_member_message__question),
|
||||
text = questionText,
|
||||
confirmText = generalGetString(R.string.delete_verb),
|
||||
destructive = true,
|
||||
onConfirm = {
|
||||
deleteMessage(chatItem.id, CIDeleteMode.cidmBroadcast)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun showMsgDeliveryErrorAlert(description: String) {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(R.string.message_delivery_error_title),
|
||||
text = description,
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun PreviewChatItemView() {
|
||||
SimpleXTheme {
|
||||
ChatItemView(
|
||||
ChatInfo.Direct.sampleData,
|
||||
ChatItem.getSampleData(
|
||||
1, CIDirection.DirectSnd(), Clock.System.now(), "hello"
|
||||
),
|
||||
useLinkPreviews = true,
|
||||
linkMode = SimplexLinkMode.DESCRIPTION,
|
||||
composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = true)) },
|
||||
deleteMessage = { _, _ -> },
|
||||
receiveFile = {},
|
||||
cancelFile = {},
|
||||
joinGroup = {},
|
||||
acceptCall = { _ -> },
|
||||
scrollToItem = {},
|
||||
acceptFeature = { _, _, _ -> }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun PreviewChatItemViewDeletedContent() {
|
||||
SimpleXTheme {
|
||||
ChatItemView(
|
||||
ChatInfo.Direct.sampleData,
|
||||
ChatItem.getDeletedContentSampleData(),
|
||||
useLinkPreviews = true,
|
||||
linkMode = SimplexLinkMode.DESCRIPTION,
|
||||
composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = true)) },
|
||||
deleteMessage = { _, _ -> },
|
||||
receiveFile = {},
|
||||
cancelFile = {},
|
||||
joinGroup = {},
|
||||
acceptCall = { _ -> },
|
||||
scrollToItem = {},
|
||||
acceptFeature = { _, _, _ -> }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -14,18 +14,18 @@ import androidx.compose.ui.graphics.asImageBitmap
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.layout.*
|
||||
import androidx.compose.ui.platform.UriHandler
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.*
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.*
|
||||
import androidx.compose.ui.unit.*
|
||||
import androidx.compose.ui.util.fastMap
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.model.*
|
||||
import chat.simplex.app.ui.theme.*
|
||||
import chat.simplex.app.views.helpers.*
|
||||
import chat.simplex.res.MR
|
||||
import kotlinx.datetime.Clock
|
||||
import kotlin.math.min
|
||||
|
||||
@@ -119,7 +119,7 @@ fun FramedItemView(
|
||||
val imageBitmap = base64ToBitmap(qi.content.image).asImageBitmap()
|
||||
Image(
|
||||
imageBitmap,
|
||||
contentDescription = stringResource(MR.strings.image_descr),
|
||||
contentDescription = stringResource(R.string.image_descr),
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier.size(68.dp).clipToBounds()
|
||||
)
|
||||
@@ -131,7 +131,7 @@ fun FramedItemView(
|
||||
val imageBitmap = base64ToBitmap(qi.content.image).asImageBitmap()
|
||||
Image(
|
||||
imageBitmap,
|
||||
contentDescription = stringResource(MR.strings.video_descr),
|
||||
contentDescription = stringResource(R.string.video_descr),
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier.size(68.dp).clipToBounds()
|
||||
)
|
||||
@@ -141,8 +141,8 @@ fun FramedItemView(
|
||||
ciQuotedMsgView(qi)
|
||||
}
|
||||
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) stringResource(MR.strings.icon_descr_file) else stringResource(MR.strings.voice_message),
|
||||
if (qi.content is MsgContent.MCFile) painterResource(R.drawable.ic_draft_filled) else painterResource(R.drawable.ic_mic_filled),
|
||||
if (qi.content is MsgContent.MCFile) stringResource(R.string.icon_descr_file) else stringResource(R.string.voice_message),
|
||||
Modifier
|
||||
.padding(top = 6.dp, end = 4.dp)
|
||||
.size(22.dp),
|
||||
@@ -182,12 +182,12 @@ fun FramedItemView(
|
||||
PriorityLayout(Modifier, CHAT_IMAGE_LAYOUT_ID) {
|
||||
if (ci.meta.itemDeleted != null) {
|
||||
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 {
|
||||
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) {
|
||||
FramedItemHeader(stringResource(MR.strings.live), false)
|
||||
FramedItemHeader(stringResource(R.string.live), false)
|
||||
}
|
||||
ci.quotedItem?.let { ciQuoteView(it) }
|
||||
if (ci.file == null && ci.formattedText == null && !ci.meta.isLive && isShortEmoji(ci.content.text)) {
|
||||
@@ -221,7 +221,7 @@ fun FramedItemView(
|
||||
}
|
||||
}
|
||||
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 != "") {
|
||||
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
|
||||
// Expecting only one such element. Can be less than one but not more
|
||||
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)
|
||||
imagePlaceable!!
|
||||
else
|
||||
@@ -19,7 +19,7 @@ import androidx.compose.ui.input.pointer.*
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
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.viewinterop.AndroidView
|
||||
import androidx.core.view.isVisible
|
||||
@@ -35,7 +35,6 @@ import coil.size.Size
|
||||
import com.google.accompanist.pager.*
|
||||
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout
|
||||
import com.google.android.exoplayer2.ui.StyledPlayerView
|
||||
import chat.simplex.res.MR
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.launch
|
||||
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
|
||||
imageLoader = imageLoader
|
||||
),
|
||||
contentDescription = stringResource(MR.strings.image_descr),
|
||||
contentDescription = stringResource(R.string.image_descr),
|
||||
contentScale = ContentScale.Fit,
|
||||
modifier = modifier,
|
||||
)
|
||||
@@ -179,7 +178,7 @@ fun ImageFullScreenView(imageProvider: () -> ImageGalleryProvider, close: () ->
|
||||
@Composable
|
||||
private fun VideoView(modifier: Modifier, uri: Uri, defaultPreview: Bitmap, currentPage: Boolean) {
|
||||
val context = LocalContext.current
|
||||
val player = remember(uri) { VideoPlayer.getOrCreate(uri, true, defaultPreview, 0L, true) }
|
||||
val player = remember(uri) { VideoPlayer.getOrCreate(uri, true, defaultPreview, 0L, true, context) }
|
||||
val isCurrentPage = rememberUpdatedState(currentPage)
|
||||
val play = {
|
||||
player.play(true)
|
||||
@@ -23,7 +23,6 @@ import chat.simplex.app.ui.theme.CurrentColors
|
||||
import chat.simplex.app.ui.theme.SimpleXTheme
|
||||
import chat.simplex.app.views.helpers.AlertManager
|
||||
import chat.simplex.app.views.helpers.generalGetString
|
||||
import chat.simplex.res.MR
|
||||
|
||||
@Composable
|
||||
fun IntegrityErrorItemView(msgError: MsgErrorType, ci: ChatItem, timedMessagesTTL: Int?, showMember: Boolean = false) {
|
||||
@@ -31,21 +30,21 @@ fun IntegrityErrorItemView(msgError: MsgErrorType, ci: ChatItem, timedMessagesTT
|
||||
when (msgError) {
|
||||
is MsgErrorType.MsgSkipped ->
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(MR.strings.alert_title_skipped_messages),
|
||||
text = generalGetString(MR.strings.alert_text_skipped_messages_it_can_happen_when)
|
||||
title = generalGetString(R.string.alert_title_skipped_messages),
|
||||
text = generalGetString(R.string.alert_text_skipped_messages_it_can_happen_when)
|
||||
)
|
||||
is MsgErrorType.MsgBadHash ->
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(MR.strings.alert_title_msg_bad_hash),
|
||||
text = generalGetString(MR.strings.alert_text_msg_bad_hash) + "\n" +
|
||||
generalGetString(MR.strings.alert_text_fragment_encryption_out_of_sync_old_database) + "\n" +
|
||||
generalGetString(MR.strings.alert_text_fragment_please_report_to_developers)
|
||||
title = generalGetString(R.string.alert_title_msg_bad_hash),
|
||||
text = generalGetString(R.string.alert_text_msg_bad_hash) + "\n" +
|
||||
generalGetString(R.string.alert_text_fragment_encryption_out_of_sync_old_database) + "\n" +
|
||||
generalGetString(R.string.alert_text_fragment_please_report_to_developers)
|
||||
)
|
||||
is MsgErrorType.MsgBadId, is MsgErrorType.MsgDuplicate ->
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(MR.strings.alert_title_msg_bad_id),
|
||||
text = generalGetString(MR.strings.alert_text_msg_bad_id) + "\n" +
|
||||
generalGetString(MR.strings.alert_text_fragment_please_report_to_developers)
|
||||
title = generalGetString(R.string.alert_title_msg_bad_id),
|
||||
text = generalGetString(R.string.alert_text_msg_bad_id) + "\n" +
|
||||
generalGetString(R.string.alert_text_fragment_please_report_to_developers)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -19,8 +19,6 @@ import chat.simplex.app.model.CIDeleted
|
||||
import chat.simplex.app.model.ChatItem
|
||||
import chat.simplex.app.ui.theme.*
|
||||
import chat.simplex.app.views.helpers.generalGetString
|
||||
import chat.simplex.res.MR
|
||||
import kotlinx.datetime.Clock
|
||||
|
||||
@Composable
|
||||
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)) {
|
||||
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 {
|
||||
MarkedDeletedText(generalGetString(MR.strings.marked_deleted_description))
|
||||
MarkedDeletedText(generalGetString(R.string.marked_deleted_description))
|
||||
}
|
||||
}
|
||||
CIMetaView(ci, timedMessagesTTL)
|
||||
@@ -69,7 +67,7 @@ private fun MarkedDeletedText(text: String) {
|
||||
fun PreviewMarkedDeletedItemView() {
|
||||
SimpleXTheme {
|
||||
DeletedItemView(
|
||||
ChatItem.getSampleData(itemDeleted = CIDeleted.Deleted(Clock.System.now())),
|
||||
ChatItem.getSampleData(itemDeleted = CIDeleted.Deleted()),
|
||||
null
|
||||
)
|
||||
}
|
||||
@@ -4,11 +4,12 @@ import android.content.res.Configuration
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
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.ui.Alignment
|
||||
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.font.FontWeight
|
||||
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.ui.theme.SimpleXTheme
|
||||
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.simplexTeamUri
|
||||
import chat.simplex.res.MR
|
||||
|
||||
val bold = SpanStyle(fontWeight = FontWeight.Bold)
|
||||
|
||||
@@ -29,14 +29,23 @@ fun ChatHelpView(addContact: (() -> Unit)? = null) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp)
|
||||
) {
|
||||
Text(stringResource(MR.strings.thank_you_for_installing_simplex), lineHeight = 22.sp)
|
||||
ReadableTextWithLink(MR.strings.you_can_connect_to_simplex_chat_founder, simplexTeamUri)
|
||||
val uriHandler = LocalUriHandler.current
|
||||
|
||||
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(
|
||||
Modifier.padding(top = 24.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp)
|
||||
) {
|
||||
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,
|
||||
lineHeight = 22.sp
|
||||
)
|
||||
@@ -44,33 +53,33 @@ fun ChatHelpView(addContact: (() -> Unit)? = null) {
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Text(stringResource(MR.strings.chat_help_tap_button))
|
||||
Text(stringResource(R.string.chat_help_tap_button))
|
||||
Icon(
|
||||
painterResource(MR.images.ic_person_add),
|
||||
stringResource(MR.strings.add_contact),
|
||||
painterResource(R.drawable.ic_person_add),
|
||||
stringResource(R.string.add_contact),
|
||||
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(MR.strings.scan_QR_code_to_connect_to_contact_who_shows_QR_code), lineHeight = 22.sp)
|
||||
Text(annotatedStringResource(R.string.add_new_contact_to_create_one_time_QR_code), lineHeight = 22.sp)
|
||||
Text(annotatedStringResource(R.string.scan_QR_code_to_connect_to_contact_who_shows_QR_code), lineHeight = 22.sp)
|
||||
}
|
||||
|
||||
Column(
|
||||
Modifier.padding(top = 24.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp)
|
||||
) {
|
||||
Text(stringResource(MR.strings.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(annotatedStringResource(MR.strings.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(stringResource(R.string.to_connect_via_link_title), style = MaterialTheme.typography.h2)
|
||||
Text(stringResource(R.string.if_you_received_simplex_invitation_link_you_can_open_in_browser), lineHeight = 22.sp)
|
||||
Text(annotatedStringResource(R.string.desktop_scan_QR_code_from_app_via_scan_QR_code), lineHeight = 22.sp)
|
||||
Text(annotatedStringResource(R.string.mobile_tap_open_in_mobile_app_then_tap_connect_in_app), lineHeight = 22.sp)
|
||||
}
|
||||
|
||||
Column(
|
||||
Modifier.padding(vertical = 24.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()
|
||||
}
|
||||
}
|
||||
@@ -9,13 +9,14 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
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.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
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.*
|
||||
import chat.simplex.app.ui.theme.*
|
||||
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.helpers.*
|
||||
import chat.simplex.app.views.newchat.ContactConnectionInfoView
|
||||
import chat.simplex.res.MR
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.datetime.Clock
|
||||
|
||||
@@ -134,19 +134,8 @@ suspend fun apiFindMessages(chatInfo: ChatInfo, chatModel: ChatModel, search: St
|
||||
|
||||
suspend fun setGroupMembers(groupInfo: GroupInfo, chatModel: ChatModel) {
|
||||
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.addAll(newMembers)
|
||||
chatModel.groupMembers.addAll(groupMembers)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -156,7 +145,6 @@ fun ContactMenuItems(chat: Chat, chatModel: ChatModel, showMenu: MutableState<Bo
|
||||
} else {
|
||||
MarkUnreadChatAction(chat, chatModel, showMenu)
|
||||
}
|
||||
ToggleFavoritesChatAction(chat, chatModel, chat.chatInfo.chatSettings?.favorite == true, showMenu)
|
||||
ToggleNotificationsChatAction(chat, chatModel, chat.chatInfo.ntfsEnabled, showMenu)
|
||||
ClearChatAction(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)
|
||||
}
|
||||
}
|
||||
GroupMemberStatus.MemAccepted -> {
|
||||
if (groupInfo.membership.memberCurrent) {
|
||||
LeaveGroupAction(groupInfo, chatModel, showMenu)
|
||||
}
|
||||
if (groupInfo.canDelete) {
|
||||
DeleteGroupAction(chat, groupInfo, chatModel, showMenu)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
if (showMarkRead) {
|
||||
MarkReadChatAction(chat, chatModel, showMenu)
|
||||
} else {
|
||||
MarkUnreadChatAction(chat, chatModel, showMenu)
|
||||
}
|
||||
ToggleFavoritesChatAction(chat, chatModel, chat.chatInfo.chatSettings?.favorite == true, showMenu)
|
||||
ToggleNotificationsChatAction(chat, chatModel, chat.chatInfo.ntfsEnabled, showMenu)
|
||||
ClearChatAction(chat, chatModel, showMenu)
|
||||
if (groupInfo.membership.memberCurrent) {
|
||||
@@ -201,8 +180,8 @@ fun GroupMenuItems(chat: Chat, groupInfo: GroupInfo, chatModel: ChatModel, showM
|
||||
@Composable
|
||||
fun MarkReadChatAction(chat: Chat, chatModel: ChatModel, showMenu: MutableState<Boolean>) {
|
||||
ItemAction(
|
||||
stringResource(MR.strings.mark_read),
|
||||
painterResource(MR.images.ic_check),
|
||||
stringResource(R.string.mark_read),
|
||||
painterResource(R.drawable.ic_check),
|
||||
onClick = {
|
||||
markChatRead(chat, chatModel)
|
||||
chatModel.controller.ntfManager.cancelNotificationsForChat(chat.id)
|
||||
@@ -214,8 +193,8 @@ fun MarkReadChatAction(chat: Chat, chatModel: ChatModel, showMenu: MutableState<
|
||||
@Composable
|
||||
fun MarkUnreadChatAction(chat: Chat, chatModel: ChatModel, showMenu: MutableState<Boolean>) {
|
||||
ItemAction(
|
||||
stringResource(MR.strings.mark_unread),
|
||||
painterResource(MR.images.ic_mark_chat_unread),
|
||||
stringResource(R.string.mark_unread),
|
||||
painterResource(R.drawable.ic_mark_chat_unread),
|
||||
onClick = {
|
||||
markChatUnread(chat, chatModel)
|
||||
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
|
||||
fun ToggleNotificationsChatAction(chat: Chat, chatModel: ChatModel, ntfsEnabled: Boolean, showMenu: MutableState<Boolean>) {
|
||||
ItemAction(
|
||||
if (ntfsEnabled) stringResource(MR.strings.mute_chat) else stringResource(MR.strings.unmute_chat),
|
||||
if (ntfsEnabled) painterResource(MR.images.ic_notifications_off) else painterResource(MR.images.ic_notifications),
|
||||
if (ntfsEnabled) stringResource(R.string.mute_chat) else stringResource(R.string.unmute_chat),
|
||||
if (ntfsEnabled) painterResource(R.drawable.ic_notifications_off) else painterResource(R.drawable.ic_notifications),
|
||||
onClick = {
|
||||
toggleNotifications(chat, !ntfsEnabled, chatModel)
|
||||
changeNtfsStatePerChat(!ntfsEnabled, mutableStateOf(ntfsEnabled), chat, chatModel)
|
||||
showMenu.value = false
|
||||
}
|
||||
)
|
||||
@@ -250,8 +217,8 @@ fun ToggleNotificationsChatAction(chat: Chat, chatModel: ChatModel, ntfsEnabled:
|
||||
@Composable
|
||||
fun ClearChatAction(chat: Chat, chatModel: ChatModel, showMenu: MutableState<Boolean>) {
|
||||
ItemAction(
|
||||
stringResource(MR.strings.clear_chat_menu_action),
|
||||
painterResource(MR.images.ic_settings_backup_restore),
|
||||
stringResource(R.string.clear_chat_menu_action),
|
||||
painterResource(R.drawable.ic_settings_backup_restore),
|
||||
onClick = {
|
||||
clearChatDialog(chat.chatInfo, chatModel)
|
||||
showMenu.value = false
|
||||
@@ -263,8 +230,8 @@ fun ClearChatAction(chat: Chat, chatModel: ChatModel, showMenu: MutableState<Boo
|
||||
@Composable
|
||||
fun DeleteContactAction(chat: Chat, chatModel: ChatModel, showMenu: MutableState<Boolean>) {
|
||||
ItemAction(
|
||||
stringResource(MR.strings.delete_contact_menu_action),
|
||||
painterResource(MR.images.ic_delete),
|
||||
stringResource(R.string.delete_contact_menu_action),
|
||||
painterResource(R.drawable.ic_delete),
|
||||
onClick = {
|
||||
deleteContactDialog(chat.chatInfo, chatModel)
|
||||
showMenu.value = false
|
||||
@@ -276,8 +243,8 @@ fun DeleteContactAction(chat: Chat, chatModel: ChatModel, showMenu: MutableState
|
||||
@Composable
|
||||
fun DeleteGroupAction(chat: Chat, groupInfo: GroupInfo, chatModel: ChatModel, showMenu: MutableState<Boolean>) {
|
||||
ItemAction(
|
||||
stringResource(MR.strings.delete_group_menu_action),
|
||||
painterResource(MR.images.ic_delete),
|
||||
stringResource(R.string.delete_group_menu_action),
|
||||
painterResource(R.drawable.ic_delete),
|
||||
onClick = {
|
||||
deleteGroupDialog(chat.chatInfo, groupInfo, chatModel)
|
||||
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>) {
|
||||
val joinGroup: () -> Unit = { withApi { chatModel.controller.apiJoinGroup(groupInfo.groupId) } }
|
||||
ItemAction(
|
||||
if (chat.chatInfo.incognito) stringResource(MR.strings.join_group_incognito_button) else stringResource(MR.strings.join_group_button),
|
||||
if (chat.chatInfo.incognito) painterResource(MR.images.ic_theater_comedy_filled) else painterResource(MR.images.ic_login),
|
||||
if (chat.chatInfo.incognito) stringResource(R.string.join_group_incognito_button) else stringResource(R.string.join_group_button),
|
||||
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,
|
||||
onClick = {
|
||||
joinGroup()
|
||||
@@ -303,8 +270,8 @@ fun JoinGroupAction(chat: Chat, groupInfo: GroupInfo, chatModel: ChatModel, show
|
||||
@Composable
|
||||
fun LeaveGroupAction(groupInfo: GroupInfo, chatModel: ChatModel, showMenu: MutableState<Boolean>) {
|
||||
ItemAction(
|
||||
stringResource(MR.strings.leave_group_button),
|
||||
painterResource(MR.images.ic_logout),
|
||||
stringResource(R.string.leave_group_button),
|
||||
painterResource(R.drawable.ic_logout),
|
||||
onClick = {
|
||||
leaveGroupDialog(groupInfo, chatModel)
|
||||
showMenu.value = false
|
||||
@@ -316,8 +283,8 @@ fun LeaveGroupAction(groupInfo: GroupInfo, chatModel: ChatModel, showMenu: Mutab
|
||||
@Composable
|
||||
fun ContactRequestMenuItems(chatInfo: ChatInfo.ContactRequest, chatModel: ChatModel, showMenu: MutableState<Boolean>) {
|
||||
ItemAction(
|
||||
if (chatModel.incognito.value) stringResource(MR.strings.accept_contact_incognito_button) else stringResource(MR.strings.accept_contact_button),
|
||||
if (chatModel.incognito.value) painterResource(MR.images.ic_theater_comedy_filled) else painterResource(MR.images.ic_check),
|
||||
if (chatModel.incognito.value) stringResource(R.string.accept_contact_incognito_button) else stringResource(R.string.accept_contact_button),
|
||||
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,
|
||||
onClick = {
|
||||
acceptContactRequest(chatInfo.apiId, chatInfo, true, chatModel)
|
||||
@@ -325,8 +292,8 @@ fun ContactRequestMenuItems(chatInfo: ChatInfo.ContactRequest, chatModel: ChatMo
|
||||
}
|
||||
)
|
||||
ItemAction(
|
||||
stringResource(MR.strings.reject_contact_button),
|
||||
painterResource(MR.images.ic_close),
|
||||
stringResource(R.string.reject_contact_button),
|
||||
painterResource(R.drawable.ic_close),
|
||||
onClick = {
|
||||
rejectContactRequest(chatInfo, chatModel)
|
||||
showMenu.value = false
|
||||
@@ -338,8 +305,8 @@ fun ContactRequestMenuItems(chatInfo: ChatInfo.ContactRequest, chatModel: ChatMo
|
||||
@Composable
|
||||
fun ContactConnectionMenuItems(chatInfo: ChatInfo.ContactConnection, chatModel: ChatModel, showMenu: MutableState<Boolean>) {
|
||||
ItemAction(
|
||||
stringResource(MR.strings.set_contact_name),
|
||||
painterResource(MR.images.ic_edit),
|
||||
stringResource(R.string.set_contact_name),
|
||||
painterResource(R.drawable.ic_edit),
|
||||
onClick = {
|
||||
ModalManager.shared.showModalCloseable(true) { close ->
|
||||
ContactConnectionInfoView(chatModel, chatInfo.contactConnection.connReqInv, chatInfo.contactConnection, true, close)
|
||||
@@ -348,8 +315,8 @@ fun ContactConnectionMenuItems(chatInfo: ChatInfo.ContactConnection, chatModel:
|
||||
},
|
||||
)
|
||||
ItemAction(
|
||||
stringResource(MR.strings.delete_verb),
|
||||
painterResource(MR.images.ic_delete),
|
||||
stringResource(R.string.delete_verb),
|
||||
painterResource(R.drawable.ic_delete),
|
||||
onClick = {
|
||||
deleteContactConnectionAlert(chatInfo.contactConnection, chatModel) {}
|
||||
showMenu.value = false
|
||||
@@ -361,14 +328,14 @@ fun ContactConnectionMenuItems(chatInfo: ChatInfo.ContactConnection, chatModel:
|
||||
@Composable
|
||||
private fun InvalidDataView() {
|
||||
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(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 8.dp)
|
||||
.weight(1F)
|
||||
) {
|
||||
Text(
|
||||
stringResource(MR.strings.invalid_data),
|
||||
stringResource(R.string.invalid_data),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = MaterialTheme.typography.h3,
|
||||
@@ -425,11 +392,11 @@ fun markChatUnread(chat: Chat, chatModel: ChatModel) {
|
||||
|
||||
fun contactRequestAlertDialog(contactRequest: ChatInfo.ContactRequest, chatModel: ChatModel) {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(MR.strings.accept_connection_request__question),
|
||||
text = generalGetString(MR.strings.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),
|
||||
title = generalGetString(R.string.accept_connection_request__question),
|
||||
text = generalGetString(R.string.if_you_choose_to_reject_the_sender_will_not_be_notified),
|
||||
confirmText = if (chatModel.incognito.value) generalGetString(R.string.accept_contact_incognito_button) else generalGetString(R.string.accept_contact_button),
|
||||
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) }
|
||||
)
|
||||
}
|
||||
@@ -454,12 +421,12 @@ fun rejectContactRequest(contactRequest: ChatInfo.ContactRequest, chatModel: Cha
|
||||
fun contactConnectionAlertDialog(connection: PendingContactConnection, chatModel: ChatModel) {
|
||||
AlertManager.shared.showAlertDialogButtons(
|
||||
title = generalGetString(
|
||||
if (connection.initiated) MR.strings.you_invited_your_contact
|
||||
else MR.strings.you_accepted_connection
|
||||
if (connection.initiated) R.string.you_invited_your_contact
|
||||
else R.string.you_accepted_connection
|
||||
),
|
||||
text = generalGetString(
|
||||
if (connection.viaContactUri) MR.strings.you_will_be_connected_when_your_connection_request_is_accepted
|
||||
else MR.strings.you_will_be_connected_when_your_contacts_device_is_online
|
||||
if (connection.viaContactUri) R.string.you_will_be_connected_when_your_connection_request_is_accepted
|
||||
else R.string.you_will_be_connected_when_your_contacts_device_is_online
|
||||
),
|
||||
buttons = {
|
||||
Row(
|
||||
@@ -472,11 +439,11 @@ fun contactConnectionAlertDialog(connection: PendingContactConnection, chatModel
|
||||
AlertManager.shared.hideAlert()
|
||||
deleteContactConnectionAlert(connection, chatModel) {}
|
||||
}) {
|
||||
Text(stringResource(MR.strings.delete_verb))
|
||||
Text(stringResource(R.string.delete_verb))
|
||||
}
|
||||
Spacer(Modifier.padding(horizontal = 4.dp))
|
||||
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) {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(MR.strings.delete_pending_connection__question),
|
||||
title = generalGetString(R.string.delete_pending_connection__question),
|
||||
text = generalGetString(
|
||||
if (connection.initiated) MR.strings.contact_you_shared_link_with_wont_be_able_to_connect
|
||||
else MR.strings.connection_you_accepted_will_be_cancelled
|
||||
if (connection.initiated) R.string.contact_you_shared_link_with_wont_be_able_to_connect
|
||||
else R.string.connection_you_accepted_will_be_cancelled
|
||||
),
|
||||
confirmText = generalGetString(MR.strings.delete_verb),
|
||||
confirmText = generalGetString(R.string.delete_verb),
|
||||
onConfirm = {
|
||||
withApi {
|
||||
AlertManager.shared.hideAlert()
|
||||
@@ -506,9 +473,9 @@ fun deleteContactConnectionAlert(connection: PendingContactConnection, chatModel
|
||||
|
||||
fun pendingContactAlertDialog(chatInfo: ChatInfo, chatModel: ChatModel) {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(MR.strings.alert_title_contact_connection_pending),
|
||||
text = generalGetString(MR.strings.alert_text_connection_pending_they_need_to_be_online_can_delete_and_retry),
|
||||
confirmText = generalGetString(MR.strings.button_delete_contact),
|
||||
title = generalGetString(R.string.alert_title_contact_connection_pending),
|
||||
text = generalGetString(R.string.alert_text_connection_pending_they_need_to_be_online_can_delete_and_retry),
|
||||
confirmText = generalGetString(R.string.button_delete_contact),
|
||||
onConfirm = {
|
||||
withApi {
|
||||
val r = chatModel.controller.apiDeleteChat(chatInfo.chatType, chatInfo.apiId)
|
||||
@@ -519,26 +486,26 @@ fun pendingContactAlertDialog(chatInfo: ChatInfo, chatModel: ChatModel) {
|
||||
}
|
||||
},
|
||||
destructive = true,
|
||||
dismissText = generalGetString(MR.strings.cancel_verb),
|
||||
dismissText = generalGetString(R.string.cancel_verb),
|
||||
)
|
||||
}
|
||||
|
||||
fun acceptGroupInvitationAlertDialog(groupInfo: GroupInfo, chatModel: ChatModel) {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(MR.strings.join_group_question),
|
||||
text = generalGetString(MR.strings.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),
|
||||
title = generalGetString(R.string.join_group_question),
|
||||
text = generalGetString(R.string.you_are_invited_to_group_join_to_connect_with_group_members),
|
||||
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) } },
|
||||
dismissText = generalGetString(MR.strings.delete_verb),
|
||||
dismissText = generalGetString(R.string.delete_verb),
|
||||
onDismiss = { deleteGroup(groupInfo, chatModel) }
|
||||
)
|
||||
}
|
||||
|
||||
fun cantInviteIncognitoAlert() {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(MR.strings.alert_title_cant_invite_contacts),
|
||||
text = generalGetString(MR.strings.alert_title_cant_invite_contacts_descr),
|
||||
confirmText = generalGetString(MR.strings.ok),
|
||||
title = generalGetString(R.string.alert_title_cant_invite_contacts),
|
||||
text = generalGetString(R.string.alert_title_cant_invite_contacts_descr),
|
||||
confirmText = generalGetString(R.string.ok),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -555,28 +522,18 @@ fun deleteGroup(groupInfo: GroupInfo, chatModel: ChatModel) {
|
||||
|
||||
fun groupInvitationAcceptedAlert() {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(MR.strings.joining_group),
|
||||
generalGetString(MR.strings.youve_accepted_group_invitation_connecting_to_inviting_group_member)
|
||||
generalGetString(R.string.joining_group),
|
||||
generalGetString(R.string.youve_accepted_group_invitation_connecting_to_inviting_group_member)
|
||||
)
|
||||
}
|
||||
|
||||
fun toggleNotifications(chat: Chat, enableNtfs: Boolean, chatModel: ChatModel, currentState: MutableState<Boolean>? = null) {
|
||||
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) {
|
||||
fun changeNtfsStatePerChat(enabled: Boolean, currentState: MutableState<Boolean>, chat: Chat, chatModel: ChatModel) {
|
||||
val newChatInfo = when(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) {
|
||||
ChatInfo.Group(groupInfo.copy(chatSettings = chatSettings))
|
||||
ChatInfo.Group(groupInfo.copy(chatSettings = groupInfo.chatSettings.copy(enableNtfs = enabled)))
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
@@ -592,13 +549,10 @@ fun updateChatSettings(chat: Chat, chatSettings: ChatSettings, chatModel: ChatMo
|
||||
}
|
||||
if (res && newChatInfo != null) {
|
||||
chatModel.updateChatInfo(newChatInfo)
|
||||
if (!chatSettings.enableNtfs) {
|
||||
if (!enabled) {
|
||||
chatModel.controller.ntfManager.cancelNotificationsForChat(chat.id)
|
||||
}
|
||||
val current = currentState?.value
|
||||
if (current != null) {
|
||||
currentState.value = !current
|
||||
}
|
||||
currentState.value = enabled
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,13 +13,15 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.*
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.capitalize
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.intl.Locale
|
||||
import androidx.compose.ui.unit.*
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import chat.simplex.app.*
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.model.*
|
||||
import chat.simplex.app.ui.theme.*
|
||||
import chat.simplex.app.views.helpers.*
|
||||
@@ -28,13 +30,12 @@ import chat.simplex.app.views.onboarding.WhatsNewView
|
||||
import chat.simplex.app.views.onboarding.shouldShowWhatsNew
|
||||
import chat.simplex.app.views.usersettings.SettingsView
|
||||
import chat.simplex.app.views.usersettings.simplexTeamUri
|
||||
import chat.simplex.res.MR
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@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 userPickerState by rememberSaveable(stateSaver = AnimatedViewState.saver()) { mutableStateOf(MutableStateFlow(AnimatedViewState.GONE)) }
|
||||
val showNewChatSheet = {
|
||||
@@ -86,7 +87,7 @@ fun ChatListView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit, stopped:
|
||||
backgroundColor = if (!stopped) MaterialTheme.colors.primary else MaterialTheme.colors.secondary,
|
||||
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()) {
|
||||
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) {
|
||||
Column(Modifier.fillMaxSize().padding(DEFAULT_PADDING), horizontalAlignment = Alignment.End, verticalArrangement = Arrangement.Bottom) {
|
||||
val uriHandler = LocalUriHandler.current
|
||||
ConnectButton(generalGetString(MR.strings.chat_with_developers)) {
|
||||
ConnectButton(generalGetString(R.string.chat_with_developers)) {
|
||||
uriHandler.openUriCatching(simplexTeamUri)
|
||||
}
|
||||
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
|
||||
Canvas(modifier = Modifier.width(40.dp).height(10.dp), onDraw = {
|
||||
val trianglePath = Path().apply {
|
||||
@@ -175,10 +176,10 @@ private fun ChatListToolbar(chatModel: ChatModel, drawerState: DrawerState, user
|
||||
BackHandler(onBack = hideSearchOnBack)
|
||||
}
|
||||
val barButtons = arrayListOf<@Composable RowScope.() -> Unit>()
|
||||
if (chatModel.chats.size > 0) {
|
||||
if (chatModel.chats.size >= 8) {
|
||||
barButtons.add {
|
||||
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 {
|
||||
IconButton(onClick = {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(MR.strings.chat_is_stopped_indication),
|
||||
generalGetString(MR.strings.you_can_start_chat_via_setting_or_by_restarting_the_app)
|
||||
generalGetString(R.string.chat_is_stopped_indication),
|
||||
generalGetString(R.string.you_can_start_chat_via_setting_or_by_restarting_the_app)
|
||||
)
|
||||
}) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_report_filled),
|
||||
generalGetString(MR.strings.chat_is_stopped_indication),
|
||||
painterResource(R.drawable.ic_report_filled),
|
||||
generalGetString(R.string.chat_is_stopped_indication),
|
||||
tint = Color.Red,
|
||||
)
|
||||
}
|
||||
@@ -221,21 +222,18 @@ private fun ChatListToolbar(chatModel: ChatModel, drawerState: DrawerState, user
|
||||
},
|
||||
title = {
|
||||
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(
|
||||
stringResource(MR.strings.your_chats),
|
||||
stringResource(R.string.your_chats),
|
||||
color = MaterialTheme.colors.onBackground,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
)
|
||||
if (chatModel.chats.size > 0) {
|
||||
ToggleFilterButton()
|
||||
if (chatModel.incognito.value) {
|
||||
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
|
||||
private fun ProgressIndicator() {
|
||||
CircularProgressIndicator(
|
||||
@@ -310,12 +290,14 @@ private var lazyListState = 0 to 0
|
||||
|
||||
@Composable
|
||||
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)
|
||||
DisposableEffect(Unit) {
|
||||
onDispose { lazyListState = listState.firstVisibleItemIndex to listState.firstVisibleItemScrollOffset }
|
||||
}
|
||||
val showUnreadAndFavorites = remember { chatModel.controller.appPrefs.showUnreadAndFavorites.state }.value
|
||||
val chats by remember(search, showUnreadAndFavorites) { derivedStateOf { filteredChats(showUnreadAndFavorites, search) } }
|
||||
val chats by remember(search) { derivedStateOf { if (search.isEmpty()) chatModel.chats else chatModel.chats.filter(filter) } }
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
listState
|
||||
@@ -324,44 +306,4 @@ private fun ChatList(chatModel: ChatModel, search: String) {
|
||||
ChatListNavLinkView(chat, chatModel)
|
||||
}
|
||||
}
|
||||
if (chats.isEmpty() && !chatModel.chats.isEmpty()) {
|
||||
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
Text(generalGetString(MR.strings.no_filtered_chats), color = MaterialTheme.colors.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun filteredChats(showUnreadAndFavorites: Boolean, searchText: String): List<Chat> {
|
||||
val chatModel = SimplexApp.context.chatModel
|
||||
val s = searchText.trim().lowercase()
|
||||
return if (s.isEmpty() && !showUnreadAndFavorites)
|
||||
chatModel.chats
|
||||
else {
|
||||
chatModel.chats.filter { chat ->
|
||||
when (val cInfo = chat.chatInfo) {
|
||||
is ChatInfo.Direct -> if (s.isEmpty()) {
|
||||
filtered(chat)
|
||||
} else {
|
||||
(viewNameContains(cInfo, s) ||
|
||||
cInfo.contact.profile.displayName.lowercase().contains(s) ||
|
||||
cInfo.contact.fullName.lowercase().contains(s))
|
||||
}
|
||||
is ChatInfo.Group -> if (s.isEmpty()) {
|
||||
(filtered(chat) || cInfo.groupInfo.membership.memberStatus == GroupMemberStatus.MemInvited)
|
||||
} else {
|
||||
viewNameContains(cInfo, s)
|
||||
}
|
||||
is ChatInfo.ContactRequest -> s.isEmpty() || viewNameContains(cInfo, s)
|
||||
is ChatInfo.ContactConnection -> s.isNotEmpty() && cInfo.contactConnection.localAlias.lowercase().contains(s)
|
||||
is ChatInfo.InvalidJSON -> false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun filtered(chat: Chat): Boolean =
|
||||
(chat.chatInfo.chatSettings?.favorite ?: false) || chat.chatStats.unreadCount > 0 || chat.chatStats.unreadChat
|
||||
|
||||
private fun viewNameContains(cInfo: ChatInfo, s: String): Boolean =
|
||||
cInfo.chatViewName.lowercase().contains(s.lowercase())
|
||||
|
||||
@@ -12,8 +12,8 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.*
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
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.item.MarkdownText
|
||||
import chat.simplex.app.views.helpers.*
|
||||
import chat.simplex.res.MR
|
||||
import dev.icerock.moko.resources.ImageResource
|
||||
|
||||
@Composable
|
||||
fun ChatPreviewView(
|
||||
@@ -45,8 +43,8 @@ fun ChatPreviewView(
|
||||
@Composable
|
||||
fun groupInactiveIcon() {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_cancel_filled),
|
||||
stringResource(MR.strings.icon_descr_group_inactive),
|
||||
painterResource(R.drawable.ic_cancel_filled),
|
||||
stringResource(R.string.icon_descr_group_inactive),
|
||||
Modifier.size(18.dp).background(MaterialTheme.colors.background, CircleShape),
|
||||
tint = MaterialTheme.colors.secondary
|
||||
)
|
||||
@@ -78,15 +76,15 @@ fun ChatPreviewView(
|
||||
|
||||
@Composable
|
||||
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 attachment(): Pair<ImageResource, String?>? =
|
||||
fun attachment(): Pair<Int, String?>? =
|
||||
when (draft.preview) {
|
||||
is ComposePreview.FilePreview -> MR.images.ic_draft_filled to draft.preview.fileName
|
||||
is ComposePreview.MediaPreview -> MR.images.ic_image to null
|
||||
is ComposePreview.VoicePreview -> MR.images.ic_play_arrow_filled to durationText(draft.preview.durationMs / 1000)
|
||||
is ComposePreview.FilePreview -> R.drawable.ic_draft_filled to draft.preview.fileName
|
||||
is ComposePreview.MediaPreview -> R.drawable.ic_image to null
|
||||
is ComposePreview.VoicePreview -> R.drawable.ic_play_arrow_filled to durationText(draft.preview.durationMs / 1000)
|
||||
else -> null
|
||||
}
|
||||
|
||||
@@ -107,12 +105,12 @@ fun ChatPreviewView(
|
||||
"editIcon" to InlineTextContent(
|
||||
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(
|
||||
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
|
||||
@@ -145,7 +143,7 @@ fun ChatPreviewView(
|
||||
val (text: CharSequence, inlineTextContent) = when {
|
||||
chatModelDraftChatId == chat.id && chatModelDraft != null -> remember(chatModelDraft) { messageDraft(chatModelDraft) }
|
||||
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 {
|
||||
chatModelDraftChatId == chat.id && chatModelDraft != null -> null
|
||||
@@ -172,12 +170,12 @@ fun ChatPreviewView(
|
||||
when (cInfo) {
|
||||
is ChatInfo.Direct ->
|
||||
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 ->
|
||||
when (cInfo.groupInfo.membership.memberStatus) {
|
||||
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 -> {}
|
||||
@@ -238,23 +236,8 @@ fun ChatPreviewView(
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_notifications_off_filled),
|
||||
contentDescription = generalGetString(MR.strings.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),
|
||||
painterResource(R.drawable.ic_notifications_off_filled),
|
||||
contentDescription = generalGetString(R.string.notifications),
|
||||
tint = MaterialTheme.colors.secondary,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 3.dp)
|
||||
@@ -278,16 +261,16 @@ fun ChatPreviewView(
|
||||
@Composable
|
||||
private fun groupInvitationPreviewText(chatModelIncognito: Boolean, currentUserProfileDisplayName: String?, groupInfo: GroupInfo): String {
|
||||
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)
|
||||
String.format(stringResource(MR.strings.group_preview_join_as), currentUserProfileDisplayName ?: "")
|
||||
String.format(stringResource(R.string.group_preview_join_as), currentUserProfileDisplayName ?: "")
|
||||
else
|
||||
stringResource(MR.strings.group_preview_you_are_invited)
|
||||
stringResource(R.string.group_preview_you_are_invited)
|
||||
}
|
||||
|
||||
@Composable
|
||||
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
|
||||
@@ -295,7 +278,7 @@ fun ChatStatusImage(s: NetworkStatus?) {
|
||||
val descr = s?.statusString
|
||||
if (s is NetworkStatus.Error) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_error),
|
||||
painterResource(R.drawable.ic_error),
|
||||
contentDescription = descr,
|
||||
tint = MaterialTheme.colors.secondary,
|
||||
modifier = Modifier
|
||||
@@ -7,21 +7,21 @@ import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
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.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.model.*
|
||||
import chat.simplex.app.ui.theme.*
|
||||
import chat.simplex.app.views.helpers.ProfileImage
|
||||
import chat.simplex.res.MR
|
||||
|
||||
@Composable
|
||||
fun ContactConnectionView(contactConnection: PendingContactConnection) {
|
||||
Row {
|
||||
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(
|
||||
modifier = Modifier
|
||||
@@ -6,7 +6,7 @@ import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
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.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -15,7 +15,6 @@ import chat.simplex.app.R
|
||||
import chat.simplex.app.model.*
|
||||
import chat.simplex.app.ui.theme.*
|
||||
import chat.simplex.app.views.helpers.ChatInfoImage
|
||||
import chat.simplex.res.MR
|
||||
|
||||
@Composable
|
||||
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
|
||||
)
|
||||
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)
|
||||
Column(
|
||||
@@ -11,8 +11,8 @@ import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.capitalize
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
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.ui.theme.*
|
||||
import chat.simplex.app.views.helpers.*
|
||||
import chat.simplex.res.MR
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
@Composable
|
||||
@@ -36,6 +35,7 @@ fun ShareListView(chatModel: ChatModel, stopped: Boolean) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.themedBackground()
|
||||
) {
|
||||
if (chatModel.chats.isNotEmpty()) {
|
||||
ShareList(chatModel, search = searchInList)
|
||||
@@ -53,7 +53,7 @@ fun ShareListView(chatModel: ChatModel, stopped: Boolean) {
|
||||
@Composable
|
||||
private fun EmptyList() {
|
||||
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) {
|
||||
barButtons.add {
|
||||
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 {
|
||||
IconButton(onClick = {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(MR.strings.chat_is_stopped_indication),
|
||||
generalGetString(MR.strings.you_can_start_chat_via_setting_or_by_restarting_the_app)
|
||||
generalGetString(R.string.chat_is_stopped_indication),
|
||||
generalGetString(R.string.you_can_start_chat_via_setting_or_by_restarting_the_app)
|
||||
)
|
||||
}) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_report_filled),
|
||||
generalGetString(MR.strings.chat_is_stopped_indication),
|
||||
painterResource(R.drawable.ic_report_filled),
|
||||
generalGetString(R.string.chat_is_stopped_indication),
|
||||
tint = Color.Red,
|
||||
)
|
||||
}
|
||||
@@ -110,18 +110,18 @@ private fun ShareListToolbar(chatModel: ChatModel, userPickerState: MutableState
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(
|
||||
when (chatModel.sharedContent.value) {
|
||||
is SharedContent.Text -> stringResource(MR.strings.share_message)
|
||||
is SharedContent.Media -> stringResource(MR.strings.share_image)
|
||||
is SharedContent.File -> stringResource(MR.strings.share_file)
|
||||
else -> stringResource(MR.strings.share_message)
|
||||
is SharedContent.Text -> stringResource(R.string.share_message)
|
||||
is SharedContent.Media -> stringResource(R.string.share_image)
|
||||
is SharedContent.File -> stringResource(R.string.share_file)
|
||||
else -> stringResource(R.string.share_message)
|
||||
},
|
||||
color = MaterialTheme.colors.onBackground,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
)
|
||||
if (chatModel.incognito.value) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_theater_comedy_filled),
|
||||
stringResource(MR.strings.incognito),
|
||||
painterResource(R.drawable.ic_theater_comedy_filled),
|
||||
stringResource(R.string.incognito),
|
||||
tint = Indigo,
|
||||
modifier = Modifier.padding(10.dp).size(26.dp)
|
||||
)
|
||||
@@ -15,7 +15,7 @@ import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
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.font.FontWeight
|
||||
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.ui.theme.*
|
||||
import chat.simplex.app.views.helpers.*
|
||||
import chat.simplex.res.MR
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -168,9 +167,9 @@ fun UserProfilePickerItem(u: User, unreadCount: Int = 0, padding: PaddingValues
|
||||
) {
|
||||
UserProfileRow(u)
|
||||
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) {
|
||||
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) {
|
||||
Box(
|
||||
contentAlignment = Alignment.Center
|
||||
@@ -186,7 +185,7 @@ fun UserProfilePickerItem(u: User, unreadCount: Int = 0, padding: PaddingValues
|
||||
)
|
||||
}
|
||||
} 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 {
|
||||
Box(Modifier.size(20.dp))
|
||||
}
|
||||
@@ -218,8 +217,8 @@ fun UserProfileRow(u: User) {
|
||||
@Composable
|
||||
private fun SettingsPickerItem(onClick: () -> Unit) {
|
||||
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)
|
||||
Icon(painterResource(MR.images.ic_settings), text, Modifier.size(20.dp), tint = MaterialTheme.colors.onBackground)
|
||||
val text = generalGetString(R.string.settings_section_title_settings).lowercase().capitalize(Locale.current)
|
||||
Icon(painterResource(R.drawable.ic_settings), text, Modifier.size(20.dp), tint = MaterialTheme.colors.onBackground)
|
||||
Spacer(Modifier.width(DEFAULT_PADDING + 6.dp))
|
||||
Text(
|
||||
text,
|
||||
@@ -231,8 +230,8 @@ private fun SettingsPickerItem(onClick: () -> Unit) {
|
||||
@Composable
|
||||
private fun CancelPickerItem(onClick: () -> Unit) {
|
||||
SectionItemView(onClick, padding = PaddingValues(start = DEFAULT_PADDING + 7.dp, end = DEFAULT_PADDING), minHeight = 68.dp) {
|
||||
val text = generalGetString(MR.strings.cancel_verb)
|
||||
Icon(painterResource(MR.images.ic_close), text, Modifier.size(20.dp), tint = MaterialTheme.colors.onBackground)
|
||||
val text = generalGetString(R.string.cancel_verb)
|
||||
Icon(painterResource(R.drawable.ic_close), text, Modifier.size(20.dp), tint = MaterialTheme.colors.onBackground)
|
||||
Spacer(Modifier.width(DEFAULT_PADDING + 6.dp))
|
||||
Text(
|
||||
text,
|
||||
@@ -14,19 +14,19 @@ import androidx.activity.result.contract.ActivityResultContracts
|
||||
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.platform.LocalContext
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import chat.simplex.app.*
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.TAG
|
||||
import chat.simplex.app.model.ChatModel
|
||||
import chat.simplex.app.ui.theme.SimpleXTheme
|
||||
import chat.simplex.app.views.helpers.*
|
||||
import chat.simplex.app.views.usersettings.*
|
||||
import chat.simplex.res.MR
|
||||
import kotlinx.datetime.*
|
||||
import java.io.BufferedOutputStream
|
||||
import java.io.File
|
||||
@@ -36,8 +36,8 @@ import java.util.*
|
||||
@Composable
|
||||
fun ChatArchiveView(m: ChatModel, title: String, archiveName: String, archiveTime: Instant) {
|
||||
val context = LocalContext.current
|
||||
val archivePath = "${getFilesDirectory()}/$archiveName"
|
||||
val saveArchiveLauncher = rememberSaveArchiveLauncher(archivePath)
|
||||
val archivePath = "${getFilesDirectory(context)}/$archiveName"
|
||||
val saveArchiveLauncher = rememberSaveArchiveLauncher(cxt = context, archivePath)
|
||||
ChatArchiveLayout(
|
||||
title,
|
||||
archiveTime,
|
||||
@@ -57,17 +57,17 @@ fun ChatArchiveLayout(
|
||||
Modifier.fillMaxWidth(),
|
||||
) {
|
||||
AppBarTitle(title)
|
||||
SectionView(stringResource(MR.strings.chat_archive_section)) {
|
||||
SectionView(stringResource(R.string.chat_archive_section)) {
|
||||
SettingsActionItem(
|
||||
painterResource(MR.images.ic_ios_share),
|
||||
stringResource(MR.strings.save_archive),
|
||||
painterResource(R.drawable.ic_ios_share),
|
||||
stringResource(R.string.save_archive),
|
||||
saveArchive,
|
||||
textColor = MaterialTheme.colors.primary,
|
||||
iconColor = MaterialTheme.colors.primary,
|
||||
)
|
||||
SettingsActionItem(
|
||||
painterResource(MR.images.ic_delete),
|
||||
stringResource(MR.strings.delete_archive),
|
||||
painterResource(R.drawable.ic_delete),
|
||||
stringResource(R.string.delete_archive),
|
||||
deleteArchiveAlert,
|
||||
textColor = 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()))
|
||||
SectionTextFooter(
|
||||
String.format(generalGetString(MR.strings.archive_created_on_ts), archiveTs)
|
||||
String.format(generalGetString(R.string.archive_created_on_ts), archiveTs)
|
||||
)
|
||||
SectionBottomSpacer()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun rememberSaveArchiveLauncher(chatArchivePath: String): ManagedActivityResultLauncher<String, Uri?> =
|
||||
private fun rememberSaveArchiveLauncher(cxt: Context, chatArchivePath: String): ManagedActivityResultLauncher<String, Uri?> =
|
||||
rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.CreateDocument(),
|
||||
onResult = { destination ->
|
||||
val cxt = SimplexApp.context
|
||||
try {
|
||||
destination?.let {
|
||||
val contentResolver = cxt.contentResolver
|
||||
@@ -94,11 +93,11 @@ private fun rememberSaveArchiveLauncher(chatArchivePath: String): ManagedActivit
|
||||
val outputStream = BufferedOutputStream(stream)
|
||||
File(chatArchivePath).inputStream().use { it.copyTo(outputStream) }
|
||||
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) {
|
||||
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")
|
||||
}
|
||||
}
|
||||
@@ -106,8 +105,8 @@ private fun rememberSaveArchiveLauncher(chatArchivePath: String): ManagedActivit
|
||||
|
||||
private fun deleteArchiveAlert(m: ChatModel, archivePath: String) {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(MR.strings.delete_chat_archive_question),
|
||||
confirmText = generalGetString(MR.strings.delete_verb),
|
||||
title = generalGetString(R.string.delete_chat_archive_question),
|
||||
confirmText = generalGetString(R.string.delete_verb),
|
||||
onConfirm = {
|
||||
val fileDeleted = File(archivePath).delete()
|
||||
if (fileDeleted) {
|
||||
@@ -18,8 +18,8 @@ import androidx.compose.ui.*
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.*
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.*
|
||||
@@ -30,7 +30,6 @@ 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.res.MR
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.datetime.Clock
|
||||
import kotlin.math.log2
|
||||
@@ -72,14 +71,14 @@ fun DatabaseEncryptionView(m: ChatModel) {
|
||||
sqliteError is SQLiteError.ErrorNotADatabase -> {
|
||||
operationEnded(m, progressIndicator) {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(MR.strings.wrong_passphrase_title),
|
||||
generalGetString(MR.strings.enter_correct_current_passphrase)
|
||||
generalGetString(R.string.wrong_passphrase_title),
|
||||
generalGetString(R.string.enter_correct_current_passphrase)
|
||||
)
|
||||
}
|
||||
}
|
||||
error != null -> {
|
||||
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}"
|
||||
)
|
||||
}
|
||||
@@ -92,13 +91,13 @@ fun DatabaseEncryptionView(m: ChatModel) {
|
||||
}
|
||||
resetFormAfterEncryption(m, initialRandomDBPassphrase, currentKey, newKey, confirmNewKey, storedKey, useKeychain.value)
|
||||
operationEnded(m, progressIndicator) {
|
||||
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.database_encrypted))
|
||||
AlertManager.shared.showAlertMsg(generalGetString(R.string.database_encrypted))
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
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(
|
||||
Modifier.fillMaxWidth().verticalScroll(rememberScrollState()),
|
||||
) {
|
||||
AppBarTitle(stringResource(MR.strings.database_passphrase))
|
||||
AppBarTitle(stringResource(R.string.database_passphrase))
|
||||
SectionView(null) {
|
||||
SavePassphraseSetting(useKeychain.value, initialRandomDBPassphrase.value, storedKey.value, progressIndicator.value) { checked ->
|
||||
if (checked) {
|
||||
setUseKeychain(true, useKeychain, prefs)
|
||||
} else if (storedKey.value) {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(MR.strings.remove_passphrase_from_keychain),
|
||||
text = generalGetString(MR.strings.notifications_will_be_hidden) + "\n" + storeSecurelyDanger(),
|
||||
confirmText = generalGetString(MR.strings.remove_passphrase),
|
||||
title = generalGetString(R.string.remove_passphrase_from_keychain),
|
||||
text = generalGetString(R.string.notifications_will_be_hidden) + "\n" + storeSecurelyDanger(),
|
||||
confirmText = generalGetString(R.string.remove_passphrase),
|
||||
onConfirm = {
|
||||
DatabaseUtils.ksDatabasePassword.remove()
|
||||
setUseKeychain(false, useKeychain, prefs)
|
||||
@@ -162,7 +161,7 @@ fun DatabaseEncryptionLayout(
|
||||
if (!initialRandomDBPassphrase.value && chatDbEncrypted == true) {
|
||||
PassphraseField(
|
||||
currentKey,
|
||||
generalGetString(MR.strings.current_passphrase),
|
||||
generalGetString(R.string.current_passphrase),
|
||||
modifier = Modifier.padding(horizontal = DEFAULT_PADDING),
|
||||
isValid = ::validKey,
|
||||
keyboardActions = KeyboardActions(onNext = { defaultKeyboardAction(ImeAction.Next) }),
|
||||
@@ -171,7 +170,7 @@ fun DatabaseEncryptionLayout(
|
||||
|
||||
PassphraseField(
|
||||
newKey,
|
||||
generalGetString(MR.strings.new_passphrase),
|
||||
generalGetString(R.string.new_passphrase),
|
||||
modifier = Modifier.padding(horizontal = DEFAULT_PADDING),
|
||||
showStrength = true,
|
||||
isValid = ::validKey,
|
||||
@@ -202,7 +201,7 @@ fun DatabaseEncryptionLayout(
|
||||
|
||||
PassphraseField(
|
||||
confirmNewKey,
|
||||
generalGetString(MR.strings.confirm_new_passphrase),
|
||||
generalGetString(R.string.confirm_new_passphrase),
|
||||
modifier = Modifier.padding(horizontal = DEFAULT_PADDING),
|
||||
isValid = { confirmNewKey.value == "" || newKey.value == confirmNewKey.value },
|
||||
keyboardActions = KeyboardActions(onDone = {
|
||||
@@ -212,27 +211,27 @@ fun DatabaseEncryptionLayout(
|
||||
)
|
||||
|
||||
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 {
|
||||
if (chatDbEncrypted == false) {
|
||||
SectionTextFooter(generalGetString(MR.strings.database_is_not_encrypted))
|
||||
SectionTextFooter(generalGetString(R.string.database_is_not_encrypted))
|
||||
} else if (useKeychain.value) {
|
||||
if (storedKey.value) {
|
||||
SectionTextFooter(generalGetString(MR.strings.keychain_is_storing_securely))
|
||||
SectionTextFooter(generalGetString(R.string.keychain_is_storing_securely))
|
||||
if (initialRandomDBPassphrase.value) {
|
||||
SectionTextFooter(generalGetString(MR.strings.encrypted_with_random_passphrase))
|
||||
SectionTextFooter(generalGetString(R.string.encrypted_with_random_passphrase))
|
||||
} else {
|
||||
SectionTextFooter(generalGetString(MR.strings.impossible_to_recover_passphrase))
|
||||
SectionTextFooter(generalGetString(R.string.impossible_to_recover_passphrase))
|
||||
}
|
||||
} else {
|
||||
SectionTextFooter(generalGetString(MR.strings.keychain_allows_to_receive_ntfs))
|
||||
SectionTextFooter(generalGetString(R.string.keychain_allows_to_receive_ntfs))
|
||||
}
|
||||
} else {
|
||||
SectionTextFooter(generalGetString(MR.strings.you_have_to_enter_passphrase_every_time))
|
||||
SectionTextFooter(generalGetString(MR.strings.impossible_to_recover_passphrase))
|
||||
SectionTextFooter(generalGetString(R.string.you_have_to_enter_passphrase_every_time))
|
||||
SectionTextFooter(generalGetString(R.string.impossible_to_recover_passphrase))
|
||||
}
|
||||
}
|
||||
SectionBottomSpacer()
|
||||
@@ -241,9 +240,9 @@ fun DatabaseEncryptionLayout(
|
||||
|
||||
fun encryptDatabaseSavedAlert(onConfirm: () -> Unit) {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(MR.strings.encrypt_database_question),
|
||||
text = generalGetString(MR.strings.database_will_be_encrypted_and_passphrase_stored) + "\n" + storeSecurelySaved(),
|
||||
confirmText = generalGetString(MR.strings.encrypt_database),
|
||||
title = generalGetString(R.string.encrypt_database_question),
|
||||
text = generalGetString(R.string.database_will_be_encrypted_and_passphrase_stored) + "\n" + storeSecurelySaved(),
|
||||
confirmText = generalGetString(R.string.encrypt_database),
|
||||
onConfirm = onConfirm,
|
||||
destructive = true,
|
||||
)
|
||||
@@ -251,9 +250,9 @@ fun encryptDatabaseSavedAlert(onConfirm: () -> Unit) {
|
||||
|
||||
fun encryptDatabaseAlert(onConfirm: () -> Unit) {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(MR.strings.encrypt_database_question),
|
||||
text = generalGetString(MR.strings.database_will_be_encrypted) +"\n" + storeSecurelyDanger(),
|
||||
confirmText = generalGetString(MR.strings.encrypt_database),
|
||||
title = generalGetString(R.string.encrypt_database_question),
|
||||
text = generalGetString(R.string.database_will_be_encrypted) +"\n" + storeSecurelyDanger(),
|
||||
confirmText = generalGetString(R.string.encrypt_database),
|
||||
onConfirm = onConfirm,
|
||||
destructive = true,
|
||||
)
|
||||
@@ -261,9 +260,9 @@ fun encryptDatabaseAlert(onConfirm: () -> Unit) {
|
||||
|
||||
fun changeDatabaseKeySavedAlert(onConfirm: () -> Unit) {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(MR.strings.change_database_passphrase_question),
|
||||
text = generalGetString(MR.strings.database_encryption_will_be_updated) + "\n" + storeSecurelySaved(),
|
||||
confirmText = generalGetString(MR.strings.update_database),
|
||||
title = generalGetString(R.string.change_database_passphrase_question),
|
||||
text = generalGetString(R.string.database_encryption_will_be_updated) + "\n" + storeSecurelySaved(),
|
||||
confirmText = generalGetString(R.string.update_database),
|
||||
onConfirm = onConfirm,
|
||||
destructive = false,
|
||||
)
|
||||
@@ -271,9 +270,9 @@ fun changeDatabaseKeySavedAlert(onConfirm: () -> Unit) {
|
||||
|
||||
fun changeDatabaseKeyAlert(onConfirm: () -> Unit) {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(MR.strings.change_database_passphrase_question),
|
||||
text = generalGetString(MR.strings.database_passphrase_will_be_updated) + "\n" + storeSecurelyDanger(),
|
||||
confirmText = generalGetString(MR.strings.update_database),
|
||||
title = generalGetString(R.string.change_database_passphrase_question),
|
||||
text = generalGetString(R.string.database_passphrase_will_be_updated) + "\n" + storeSecurelyDanger(),
|
||||
confirmText = generalGetString(R.string.update_database),
|
||||
onConfirm = onConfirm,
|
||||
destructive = true,
|
||||
)
|
||||
@@ -291,13 +290,13 @@ fun SavePassphraseSetting(
|
||||
SectionItemView(minHeight = minHeight) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Icon(
|
||||
if (storedKey) painterResource(MR.images.ic_vpn_key_filled) else painterResource(MR.images.ic_vpn_key_off_filled),
|
||||
stringResource(MR.strings.save_passphrase_in_keychain),
|
||||
if (storedKey) painterResource(R.drawable.ic_vpn_key_filled) else painterResource(R.drawable.ic_vpn_key_off_filled),
|
||||
stringResource(R.string.save_passphrase_in_keychain),
|
||||
tint = if (storedKey) SimplexGreen else MaterialTheme.colors.secondary
|
||||
)
|
||||
Spacer(Modifier.padding(horizontal = 4.dp))
|
||||
Text(
|
||||
stringResource(MR.strings.save_passphrase_in_keychain),
|
||||
stringResource(R.string.save_passphrase_in_keychain),
|
||||
Modifier.padding(end = 24.dp),
|
||||
color = Color.Unspecified
|
||||
)
|
||||
@@ -334,9 +333,9 @@ fun setUseKeychain(value: Boolean, useKeychain: MutableState<Boolean>, prefs: Ap
|
||||
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) {
|
||||
m.chatDbChanged.value = true
|
||||
@@ -358,8 +357,8 @@ fun PassphraseField(
|
||||
var valid by remember { mutableStateOf(validKey(key.value)) }
|
||||
var showKey by remember { mutableStateOf(false) }
|
||||
val icon = if (valid) {
|
||||
if (showKey) painterResource(MR.images.ic_visibility_off_filled) else painterResource(MR.images.ic_visibility_filled)
|
||||
} else painterResource(MR.images.ic_error)
|
||||
if (showKey) painterResource(R.drawable.ic_visibility_off_filled) else painterResource(R.drawable.ic_visibility_filled)
|
||||
} else painterResource(R.drawable.ic_error)
|
||||
val iconColor = if (valid) {
|
||||
if (showStrength && key.value.isNotEmpty()) PassphraseStrength.check(key.value).color else MaterialTheme.colors.secondary
|
||||
} else Color.Red
|
||||
@@ -498,7 +497,7 @@ fun PreviewDatabaseEncryptionLayout() {
|
||||
SimpleXTheme {
|
||||
DatabaseEncryptionLayout(
|
||||
useKeychain = remember { mutableStateOf(true) },
|
||||
prefs = AppPreferences(),
|
||||
prefs = AppPreferences(SimplexApp.context),
|
||||
chatDbEncrypted = true,
|
||||
currentKey = 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