mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-10-31 16:22:49 +00:00
Compare commits
241 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c10f4e521d | |||
| 0524b3c274 | |||
| 4187f9814d | |||
| d732f043dc | |||
| d0b4a21cfe | |||
| f9513193e2 | |||
| 62cc7e0505 | |||
| 03f3e8b3e0 | |||
| 264ec2416f | |||
| fefcb7a41d | |||
| f794cb9f06 | |||
| e915cd242b | |||
| afdf07b23e | |||
| 3bb7fe97e6 | |||
| 1619155b87 | |||
| 09b7732c1b | |||
| f9112cc583 | |||
| 03304b3ece | |||
| 3edf4a6a69 | |||
| b0c9947a11 | |||
| 14e4dd34ed | |||
| 00135bbfea | |||
| 8a54ad6e6b | |||
| 5ef90250d9 | |||
| 07ea371806 | |||
| 7f13bb95cc | |||
| 754eb3d0c1 | |||
| 68bf671f10 | |||
| 8923ffb7f1 | |||
| 3ad0ca7974 | |||
| 330dd243e2 | |||
| 3f7ed5d87a | |||
| 5f4bb30b18 | |||
| 02a40cf2d5 | |||
| 96d443e443 | |||
| 9e160e027d | |||
| 6296a538eb | |||
| b8d934058c | |||
| 7624391bdd | |||
| c7f4c96f70 | |||
| 566b0c6f86 | |||
| 101e23da38 | |||
| fad8dce166 | |||
| 3642065202 | |||
| d25362dd36 | |||
| d7a2366a60 | |||
| 3dabece5f8 | |||
| f25ca5b963 | |||
| a8b3db4621 | |||
| 6f0788fd34 | |||
| e92923a77d | |||
| c2e8912fe2 | |||
| e9abed9178 | |||
| 7f4d264ee0 | |||
| 74bc2c0f5d | |||
| 9d7ee09715 | |||
| 2e161914a9 | |||
| ebc16bb3e1 | |||
| 55f3a1acfb | |||
| a7f4418555 | |||
| ac822544f2 | |||
| 06c9c9b77c | |||
| 8731e2243a | |||
| 20a55f4840 | |||
| 28abf3049e | |||
| 6124d99273 | |||
| e0161e5b25 | |||
| 15149af6b7 | |||
| 25479480c4 | |||
| 885e042c12 | |||
| 4fd0be1fe5 | |||
| 4190939ff8 | |||
| a5d8b6eb2a | |||
| 243acf1ee0 | |||
| 3ec7aa2a55 | |||
| 387022a9af | |||
| 7c3d1e2e80 | |||
| 56ad052575 | |||
| 9672950512 | |||
| 445b9a1ee7 | |||
| 99b6151be7 | |||
| 1ddb32645c | |||
| 654f149df9 | |||
| 09c109a6cb | |||
| 30a22eeae6 | |||
| e4aee9eccd | |||
| 8f9dfb5119 | |||
| 474b039a87 | |||
| fdb2958183 | |||
| 5c7bac58ce | |||
| e346f68fc8 | |||
| e4ce5a821a | |||
| e91df7416a | |||
| e3a2e98068 | |||
| 8d9ad76531 | |||
| ad2655c87b | |||
| 31d4ba3793 | |||
| ac1be8c87a | |||
| 8840e2f96b | |||
| b6f79a883e | |||
| bad39d62e9 | |||
| 06d778859c | |||
| 762823ebd7 | |||
| 74f69b7eb3 | |||
| 4566712ad3 | |||
| ac60d0fce7 | |||
| 60837c0d7d | |||
| 301f930bcc | |||
| bfe21698f2 | |||
| 2010620fbc | |||
| 6181f60b5e | |||
| bf1ace48f2 | |||
| 25716f6f3a | |||
| 00ffff4c0a | |||
| 6b68ddcb25 | |||
| 1fdc778fe2 | |||
| 7015172df1 | |||
| f35546b473 | |||
| d8b1a41f56 | |||
| 416da32c40 | |||
| a3fbbed8f4 | |||
| 57ed76bd03 | |||
| c1dfd6d6ae | |||
| 54d452c364 | |||
| 01c8064a16 | |||
| 79f1423a30 | |||
| 4efc82fa74 | |||
| 58b3830187 | |||
| a8d6652e03 | |||
| f954a20f67 | |||
| 71ad652c38 | |||
| 6cce74db88 | |||
| 16eec4d024 | |||
| 5ca1435e9e | |||
| 29743d3c89 | |||
| 89b4722a58 | |||
| 99343926ff | |||
| 96e1c93f38 | |||
| f91fd4b535 | |||
| 5d0d8bedcd | |||
| 5f0461b1cd | |||
| 79163b68b5 | |||
| c80eb12e2f | |||
| 73ef038035 | |||
| ad84122285 | |||
| fd799a3bdf | |||
| dd3beb11bd | |||
| fb7bc50cdb | |||
| 1a5336e650 | |||
| b2a7c6548a | |||
| 95e656cc32 | |||
| b62242cdd2 | |||
| a7e6d8af6e | |||
| e282212270 | |||
| 87aff40b26 | |||
| 0aea5f94ec | |||
| f9a8acf4a5 | |||
| 29b8068d4a | |||
| 4a143c1053 | |||
| be872e6bec | |||
| 061fd82c1a | |||
| d29dad40f9 | |||
| dd24e14cf6 | |||
| 1cfd3e4dfb | |||
| 553a4785e1 | |||
| 2a0e41652a | |||
| 078b63fd7c | |||
| 889fd08b5c | |||
| 167c1ced80 | |||
| 57617a362a | |||
| 73a8a3d5d8 | |||
| eeaaac2969 | |||
| 7309e49286 | |||
| 6c34d647ca | |||
| af5a237b4a | |||
| 0528b787c9 | |||
| eca45b574e | |||
| 1640adcf9d | |||
| 42966b393f | |||
| f4c79be328 | |||
| 742902ef02 | |||
| 5697c9664f | |||
| bad32cbbda | |||
| 2902549a88 | |||
| c4c0c482f0 | |||
| b3b8467636 | |||
| 0bda1cd0d0 | |||
| 35dce84c7f | |||
| 6ca04ef772 | |||
| fa5e871e7e | |||
| 3a5e9f43b9 | |||
| 123037a612 | |||
| e98cbdfce1 | |||
| 643ece6af7 | |||
| 82c6167441 | |||
| 33e2f72792 | |||
| 5e30f94ccc | |||
| 3a2f19f26c | |||
| b226b81082 | |||
| cc736ec759 | |||
| 154a554d65 | |||
| fdc98f6b8c | |||
| 5b53686c3e | |||
| 88d419c03f | |||
| 5a2977134b | |||
| ea94996db7 | |||
| 3312526055 | |||
| bb36787371 | |||
| a201b3766e | |||
| 716d6d20b2 | |||
| 7722107001 | |||
| 5e3b1463d4 | |||
| cd64b5e376 | |||
| bfcce0e94b | |||
| 77dbd56e4e | |||
| 0e5a7e9396 | |||
| aeadd9879e | |||
| a98fd96303 | |||
| 2c2d803e58 | |||
| 40f8b48256 | |||
| d653ceb84c | |||
| 0399dab1da | |||
| bdb42759de | |||
| 257040f5ca | |||
| 8d8ac9373e | |||
| 65ed527384 | |||
| e0046e9862 | |||
| 4b479638a4 | |||
| 0daf7ecc7d | |||
| 784f5df426 | |||
| 880a189617 | |||
| aed149731b | |||
| 51816e0281 | |||
| 200b98b72b | |||
| 7148f2f28f | |||
| 78aa500d23 | |||
| af9da10254 | |||
| ef7cb35de8 | |||
| 2579e68edc | |||
| 9f26079b84 | |||
| 7f5ce76860 |
@@ -69,18 +69,18 @@ body:
|
||||
description: |
|
||||
Trigger the defect, then click on the menu from the room list then "Report a bug". Describe the issue, mentioning that you have also filed a bug (it's helpful if you can include a link to the bug). Send the report to submit anonymous logs to the developers.
|
||||
options:
|
||||
- 'Yes'
|
||||
- 'No'
|
||||
- 'Yes'
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: pr
|
||||
attributes:
|
||||
label: Are you willing to provide a PR?
|
||||
label: Will you submit a Pull Request?
|
||||
description: |
|
||||
Providing a PR can drastically speed up the process of fixing this bug. Don't worry, it's still OK to answer 'No' :).
|
||||
Submitting a Pull Request can drastically speed up the process of fixing this bug. Don't worry, it's still OK to answer 'No' :).
|
||||
options:
|
||||
- 'Yes'
|
||||
- 'No'
|
||||
- 'Yes'
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
name: Enhancement request
|
||||
description: Do you have a suggestion or feature request?
|
||||
description: Do you have a suggestion or feature request? Please open issue on the repo element-hq/element-meta if the request is global to all Element clients.
|
||||
labels: [T-Enhancement]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thank you for taking the time to propose an enhancement to an existing feature. If you would like to propose a new feature or a major cross-platform change, please [start a discussion here](https://github.com/element-hq/element-meta/discussions/new?category=ideas).
|
||||
Thank you for taking the time to propose an enhancement to an existing feature. If you would like to propose a new feature or a major cross-platform change, please open an issue on the [element-meta repository](https://github.com/element-hq/element-meta/issues/new?template=enhancement.yml) instead.
|
||||
- type: textarea
|
||||
id: usecase
|
||||
attributes:
|
||||
|
||||
@@ -51,7 +51,7 @@ jobs:
|
||||
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }}
|
||||
persist-credentials: false
|
||||
- name: Use JDK 21
|
||||
uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
|
||||
uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
|
||||
with:
|
||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||
java-version: '21'
|
||||
@@ -104,7 +104,18 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Compile release sources
|
||||
if: ${{ matrix.variant == 'release' }}
|
||||
run: ./gradlew bundleGplayRelease -PallWarningsAsErrors=true $CI_GRADLE_ARG_PROPERTIES
|
||||
run: |
|
||||
./gradlew assembleGplayRelease -PallWarningsAsErrors=true $CI_GRADLE_ARG_PROPERTIES
|
||||
./gradlew bundleGplayRelease -PallWarningsAsErrors=true $CI_GRADLE_ARG_PROPERTIES
|
||||
- name: Run aapt dump
|
||||
if: ${{ matrix.variant == 'release' }}
|
||||
run: |
|
||||
$ANDROID_HOME/build-tools/37.0.0/aapt dump badging \
|
||||
./app/build/outputs/apk/gplay/release/app-gplay-universal-release.apk \
|
||||
> /tmp/currentAaptDump.txt
|
||||
- name: Check manifest has not changed
|
||||
if: ${{ matrix.variant == 'release' }}
|
||||
run: diff /tmp/currentAaptDump.txt ./tools/manifest/gplay/release/aaptDump.txt
|
||||
- name: Compile nightly sources
|
||||
if: ${{ matrix.variant == 'nightly' }}
|
||||
run: ./gradlew compileGplayNightlySources -PallWarningsAsErrors=true $CI_GRADLE_ARG_PROPERTIES
|
||||
|
||||
@@ -56,7 +56,7 @@ jobs:
|
||||
- name: Clone submodules
|
||||
run: git submodule update --init --recursive
|
||||
- name: Use JDK 21
|
||||
uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
|
||||
uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
|
||||
with:
|
||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||
java-version: '21'
|
||||
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
- run: |
|
||||
npm install --save-dev @babel/plugin-transform-flow-strip-types
|
||||
- name: Danger
|
||||
uses: danger/danger-js@58355dc2b274a87699c8b7deac01baba16d6f061 # 13.0.8
|
||||
uses: danger/danger-js@e9b58820039524318198d8059ec98b0c02b7875c # 13.0.10
|
||||
with:
|
||||
args: "--dangerfile ./tools/danger/dangerfile.js"
|
||||
env:
|
||||
|
||||
@@ -19,7 +19,7 @@ jobs:
|
||||
- name: ⏬ Checkout with LFS
|
||||
uses: nschloe/action-cached-lfs-checkout@385a8ecc719e50b8c71af6ab01a624b486b7c3bc # v1.2.5
|
||||
- name: Use JDK 21
|
||||
uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
|
||||
uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
|
||||
with:
|
||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||
java-version: '21'
|
||||
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
|
||||
- uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
|
||||
name: Use JDK 21
|
||||
if: (github.event_name == 'pull_request' && github.event.pull_request.fork == null) || github.event_name == 'workflow_dispatch'
|
||||
with:
|
||||
|
||||
@@ -44,7 +44,7 @@ jobs:
|
||||
# https://github.com/actions/checkout/issues/881
|
||||
ref: ${{ github.ref }}
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
|
||||
- uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
|
||||
name: Use JDK 21
|
||||
with:
|
||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||
|
||||
@@ -36,7 +36,7 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Use JDK 21
|
||||
uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
|
||||
uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
|
||||
with:
|
||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||
java-version: '21'
|
||||
|
||||
@@ -37,7 +37,7 @@ jobs:
|
||||
uses: nschloe/action-cached-lfs-checkout@385a8ecc719e50b8c71af6ab01a624b486b7c3bc # v1.2.5
|
||||
|
||||
- name: Use JDK 21
|
||||
uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
|
||||
uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
|
||||
with:
|
||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||
java-version: '21'
|
||||
@@ -80,7 +80,7 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Use JDK 21
|
||||
uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
|
||||
uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
|
||||
with:
|
||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||
java-version: '21'
|
||||
|
||||
@@ -69,7 +69,7 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Use JDK 21
|
||||
uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
|
||||
uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
|
||||
with:
|
||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||
java-version: '21'
|
||||
@@ -108,7 +108,7 @@ jobs:
|
||||
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
|
||||
run: git submodule update --init --recursive
|
||||
- name: Use JDK 21
|
||||
uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
|
||||
uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
|
||||
with:
|
||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||
java-version: '21'
|
||||
@@ -149,7 +149,7 @@ jobs:
|
||||
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
|
||||
run: git submodule update --init --recursive
|
||||
- name: Use JDK 21
|
||||
uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
|
||||
uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
|
||||
with:
|
||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||
java-version: '21'
|
||||
@@ -183,7 +183,7 @@ jobs:
|
||||
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
|
||||
run: git submodule update --init --recursive
|
||||
- name: Use JDK 21
|
||||
uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
|
||||
uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
|
||||
with:
|
||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||
java-version: '21'
|
||||
@@ -228,7 +228,7 @@ jobs:
|
||||
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
|
||||
run: git submodule update --init --recursive
|
||||
- name: Use JDK 21
|
||||
uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
|
||||
uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
|
||||
with:
|
||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||
java-version: '21'
|
||||
@@ -269,7 +269,7 @@ jobs:
|
||||
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
|
||||
run: git submodule update --init --recursive
|
||||
- name: Use JDK 21
|
||||
uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
|
||||
uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
|
||||
with:
|
||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||
java-version: '21'
|
||||
@@ -360,7 +360,7 @@ jobs:
|
||||
yarn add danger-plugin-lint-report --dev
|
||||
- name: Danger lint
|
||||
if: always()
|
||||
uses: danger/danger-js@58355dc2b274a87699c8b7deac01baba16d6f061 # 13.0.8
|
||||
uses: danger/danger-js@e9b58820039524318198d8059ec98b0c02b7875c # 13.0.10
|
||||
with:
|
||||
args: "--dangerfile ./tools/danger/dangerfile-lint.js"
|
||||
env:
|
||||
|
||||
@@ -54,7 +54,7 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: ☕️ Use JDK 21
|
||||
uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
|
||||
uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
|
||||
with:
|
||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||
java-version: '21'
|
||||
|
||||
@@ -38,7 +38,7 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Use JDK 21
|
||||
uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
|
||||
uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
|
||||
with:
|
||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||
java-version: '21'
|
||||
@@ -82,7 +82,7 @@ jobs:
|
||||
- name: Clone submodules
|
||||
run: git submodule update --init --recursive
|
||||
- name: Use JDK 21
|
||||
uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
|
||||
uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
|
||||
with:
|
||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||
java-version: '21'
|
||||
@@ -126,7 +126,7 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Use JDK 21
|
||||
uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
|
||||
uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
|
||||
with:
|
||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||
java-version: '21'
|
||||
|
||||
@@ -45,7 +45,7 @@ jobs:
|
||||
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }}
|
||||
persist-credentials: false
|
||||
- name: Use JDK 21
|
||||
uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
|
||||
uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
|
||||
with:
|
||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||
java-version: '21'
|
||||
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Use JDK 21
|
||||
uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
|
||||
uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
|
||||
with:
|
||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||
java-version: '21'
|
||||
@@ -39,6 +39,7 @@ jobs:
|
||||
./tools/localazy/downloadStrings.sh --all
|
||||
./tools/localazy/importSupportedLocalesFromLocalazy.py
|
||||
./tools/test/generateAllScreenshots.py
|
||||
./tools/manifest/aaptDump.sh
|
||||
- name: Create Pull Request for Strings
|
||||
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
|
||||
with:
|
||||
|
||||
@@ -63,7 +63,7 @@ jobs:
|
||||
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
|
||||
run: git submodule update --init --recursive
|
||||
- name: ☕️ Use JDK 21
|
||||
uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
|
||||
uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
|
||||
with:
|
||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||
java-version: '21'
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
appId: ${MAESTRO_APP_ID}
|
||||
---
|
||||
# Clean up the room list if it was left in a bad state by previous tests
|
||||
- runFlow:
|
||||
when:
|
||||
visible: 'aRoomName'
|
||||
commands:
|
||||
- longPressOn: 'aRoomName'
|
||||
- tapOn:
|
||||
text: "Leave room"
|
||||
index: 0
|
||||
- tapOn: "Leave"
|
||||
- runFlow:
|
||||
when:
|
||||
visible: ${MAESTRO_INVITEE1_MXID}
|
||||
commands:
|
||||
- longPressOn: ${MAESTRO_INVITEE1_MXID}
|
||||
- tapOn:
|
||||
text: "Leave room"
|
||||
index: 0
|
||||
- tapOn: "Leave"
|
||||
@@ -7,7 +7,16 @@ appId: ${MAESTRO_APP_ID}
|
||||
- tapOn:
|
||||
text: ${MAESTRO_INVITEE1_MXID}
|
||||
index: 1
|
||||
- tapOn: "Continue"
|
||||
- runFlow:
|
||||
when:
|
||||
visible: 'Start a chat with this new contact?'
|
||||
commands:
|
||||
- tapOn: 'Continue'
|
||||
- runFlow:
|
||||
when:
|
||||
visible: 'Send invite?'
|
||||
commands:
|
||||
- tapOn: 'Send invite'
|
||||
- takeScreenshot: build/maestro/330-createAndDeleteDM
|
||||
- tapOn: "maestroelement2"
|
||||
- scroll
|
||||
|
||||
@@ -15,6 +15,12 @@ appId: ${MAESTRO_APP_ID}
|
||||
text: ${MAESTRO_INVITEE1_MXID}
|
||||
index: 1
|
||||
- tapOn: "Finish"
|
||||
- runFlow:
|
||||
when:
|
||||
visible: 'Invite new contact to this room?'
|
||||
commands:
|
||||
- tapOn:
|
||||
id: "confirm_invite_unknown"
|
||||
- tapOn: "aRoomName"
|
||||
- tapOn: "Invite"
|
||||
# assert there's 1 member and 1 invitee
|
||||
@@ -23,6 +29,10 @@ appId: ${MAESTRO_APP_ID}
|
||||
- tapOn:
|
||||
text: ${MAESTRO_INVITEE2_MXID}
|
||||
index: 1
|
||||
# Hide keyboard by tapping on an external element in case it's displayed, otherwise it will 'eat' the back button press
|
||||
- tapOn:
|
||||
# Navigation title
|
||||
text: "Invite people"
|
||||
- tapOn: "Invite"
|
||||
- runFlow:
|
||||
when:
|
||||
@@ -30,10 +40,6 @@ appId: ${MAESTRO_APP_ID}
|
||||
commands:
|
||||
- tapOn:
|
||||
id: "confirm_invite_unknown"
|
||||
# Close the keyboard if it's still open
|
||||
- tapOn: "Back"
|
||||
# Go back to the room details screen
|
||||
- tapOn: "Back"
|
||||
- scrollUntilVisible:
|
||||
direction: DOWN
|
||||
element:
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
appId: ${MAESTRO_APP_ID}
|
||||
---
|
||||
- runFlow: cleanUpRoomList.yaml
|
||||
- runFlow: searchRoomList.yaml
|
||||
- takeScreenshot: build/maestro/300-RoomList
|
||||
- runFlow: timeline/timeline.yaml
|
||||
|
||||
+49
@@ -1,3 +1,52 @@
|
||||
Changes in Element X v26.07.0
|
||||
=============================
|
||||
|
||||
<!-- Release notes generated using configuration in .github/release.yml at v26.07.0 -->
|
||||
|
||||
## What's Changed
|
||||
### ✨ Features
|
||||
* Add read all messages feature by @hghgrtut in https://github.com/element-hq/element-x-android/pull/7049
|
||||
### 🐛 Bugfixes
|
||||
* Detect RTL text in events and adjust the text rendering by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6994
|
||||
### 🗣 Translations
|
||||
* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/7071
|
||||
* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/7115
|
||||
### 🧱 Build
|
||||
* Fix Sonar with AGP 9 (some workarounds) by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7055
|
||||
* Disable periodically generating the screenshot gallery by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7110
|
||||
* Try a different workaround for flaky `ScanQrCodeViewTest` by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7109
|
||||
### 🚧 In development 🚧
|
||||
* Protect link new device by @bmarty in https://github.com/element-hq/element-x-android/pull/6992
|
||||
### Dependency upgrades
|
||||
* Update metro to v1.2.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7057
|
||||
* Update gradle/actions action to v6.2.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7063
|
||||
* Update roborazzi to v1.64.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7068
|
||||
* Update rnkdsh/action-upload-diawi action to v1.5.13 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7067
|
||||
* Update dependency io.element.android:element-call-embedded to v0.20.2 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7075
|
||||
* Update dependency com.github.matrix-org:matrix-analytics-events to v0.37.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7079
|
||||
* Update actions/setup-java action to v5.3.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7088
|
||||
* Update dependency org.matrix.rustcomponents:sdk-android to v26.06.18 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7052
|
||||
* Update firebaseAppDistribution to v5.3.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7096
|
||||
* Update dependency com.google.firebase:firebase-bom to v34.15.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7089
|
||||
* Update dependency com.google.crypto.tink:tink-android to v1.22.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7105
|
||||
* Update actions/checkout action to v7 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7098
|
||||
* Update lifecycle to v2.11.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7097
|
||||
* Update danger/danger-js action to v13.0.8 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7104
|
||||
* Update dependency org.matrix.rustcomponents:sdk-android to v26.06.25 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7107
|
||||
* Update dependency androidx.compose:compose-bom to v2026.06.00 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7095
|
||||
* Update dependency io.element.android:element-call-embedded to v0.20.3 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7117
|
||||
* Update zizmorcore/zizmor-action action to v0.5.7 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7116
|
||||
* Update dependency org.matrix.rustcomponents:sdk-android to v26.06.30 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7127
|
||||
* Update dependency com.posthog:posthog-android to v3.51.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7128
|
||||
### Others
|
||||
* Fix small code smell by @bmarty in https://github.com/element-hq/element-x-android/pull/7058
|
||||
* Hide message notification content when the app is locked by @bxdxnn in https://github.com/element-hq/element-x-android/pull/6902
|
||||
|
||||
## New Contributors
|
||||
* @hghgrtut made their first contribution in https://github.com/element-hq/element-x-android/pull/7049
|
||||
|
||||
**Full Changelog**: https://github.com/element-hq/element-x-android/compare/v26.06.4...v26.07.0
|
||||
|
||||
Changes in Element X v26.06.4
|
||||
=============================
|
||||
|
||||
|
||||
@@ -8,9 +8,6 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<!-- To be able to install APK from the application -->
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||
|
||||
<application
|
||||
android:name=".ElementXApplication"
|
||||
android:allowBackup="false"
|
||||
|
||||
@@ -15,7 +15,7 @@ import io.element.android.libraries.matrix.api.MatrixClient
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultSessionGraphFactory(
|
||||
private val appGraph: AppGraph
|
||||
private val appGraph: AppGraph,
|
||||
) : SessionGraphFactory {
|
||||
override fun create(client: MatrixClient): Any {
|
||||
return appGraph.sessionGraphFactory.create(client)
|
||||
|
||||
@@ -34,6 +34,7 @@ dependencies {
|
||||
implementation(projects.libraries.featureflag.api)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
implementation(projects.libraries.oauth.api)
|
||||
implementation(projects.libraries.permissions.api)
|
||||
implementation(projects.libraries.preferences.api)
|
||||
implementation(projects.libraries.push.api)
|
||||
implementation(projects.libraries.pushproviders.api)
|
||||
@@ -61,6 +62,7 @@ dependencies {
|
||||
testImplementation(projects.features.share.test)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
testImplementation(projects.libraries.oauth.test)
|
||||
testImplementation(projects.libraries.permissions.test)
|
||||
testImplementation(projects.libraries.preferences.test)
|
||||
testImplementation(projects.libraries.push.test)
|
||||
testImplementation(projects.libraries.pushproviders.test)
|
||||
|
||||
@@ -12,4 +12,7 @@ sealed interface LoggedInEvents {
|
||||
data class CloseErrorDialog(val doNotShowAgain: Boolean) : LoggedInEvents
|
||||
data object CheckSlidingSyncProxyAvailability : LoggedInEvents
|
||||
data object LogoutAndMigrateToNativeSlidingSync : LoggedInEvents
|
||||
data object DismissLocalNetworkPermissionPrompt : LoggedInEvents
|
||||
|
||||
data object RequestLocationNetworkPermission : LoggedInEvents
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
package io.element.android.appnav.loggedin
|
||||
|
||||
import android.Manifest
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
@@ -15,6 +16,7 @@ import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
@@ -36,6 +38,10 @@ import io.element.android.libraries.matrix.api.sync.SlidingSyncVersion
|
||||
import io.element.android.libraries.matrix.api.sync.SyncService
|
||||
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
|
||||
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
|
||||
import io.element.android.libraries.permissions.api.PermissionsEvent
|
||||
import io.element.android.libraries.permissions.api.PermissionsPresenter
|
||||
import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionAdvisor
|
||||
import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionDialog
|
||||
import io.element.android.libraries.push.api.PushService
|
||||
import io.element.android.libraries.push.api.PusherRegistrationFailure
|
||||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
@@ -58,7 +64,12 @@ class LoggedInPresenter(
|
||||
private val encryptionService: EncryptionService,
|
||||
private val buildMeta: BuildMeta,
|
||||
private val networkMonitor: NetworkMonitor,
|
||||
private val localNetworkPermissionAdvisor: LocalNetworkPermissionAdvisor,
|
||||
permissionsPresenterFactory: PermissionsPresenter.Factory,
|
||||
) : Presenter<LoggedInState> {
|
||||
private val localNetworkPermissionsPresenter: PermissionsPresenter =
|
||||
permissionsPresenterFactory.create(Manifest.permission.ACCESS_LOCAL_NETWORK)
|
||||
|
||||
@Composable
|
||||
override fun present(): LoggedInState {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
@@ -117,6 +128,21 @@ class LoggedInPresenter(
|
||||
}
|
||||
}
|
||||
|
||||
val localNetworkPermissionState = localNetworkPermissionsPresenter.present()
|
||||
var localNetworkPromptDismissedThisSession by remember { mutableStateOf(false) }
|
||||
val advisorRequestsPrompt by produceState(initialValue = false, localNetworkPermissionState.permissionGranted) {
|
||||
value = localNetworkPermissionAdvisor.shouldRequestPermissionFor(matrixClient.homeserverUrl)
|
||||
}
|
||||
LaunchedEffect(localNetworkPermissionState.permissionGranted) {
|
||||
if (localNetworkPermissionState.permissionGranted) localNetworkPromptDismissedThisSession = false
|
||||
}
|
||||
val localNetworkPermissionDialog = when {
|
||||
!advisorRequestsPrompt -> LocalNetworkPermissionDialog.None
|
||||
localNetworkPromptDismissedThisSession -> LocalNetworkPermissionDialog.None
|
||||
localNetworkPermissionState.shouldShowRationale -> LocalNetworkPermissionDialog.Rationale
|
||||
else -> LocalNetworkPermissionDialog.Settings
|
||||
}
|
||||
|
||||
fun handleEvent(event: LoggedInEvents) {
|
||||
when (event) {
|
||||
is LoggedInEvents.CloseErrorDialog -> {
|
||||
@@ -134,6 +160,17 @@ class LoggedInPresenter(
|
||||
// Force the logout since Native Sliding Sync is already enforced by the SDK
|
||||
matrixClient.logout(userInitiated = true, ignoreSdkError = true)
|
||||
}
|
||||
LoggedInEvents.DismissLocalNetworkPermissionPrompt -> {
|
||||
localNetworkPromptDismissedThisSession = true
|
||||
}
|
||||
LoggedInEvents.RequestLocationNetworkPermission -> {
|
||||
if (localNetworkPermissionDialog == LocalNetworkPermissionDialog.Settings) {
|
||||
localNetworkPermissionState.eventSink(PermissionsEvent.OpenSystemSettingAndCloseDialog)
|
||||
} else {
|
||||
localNetworkPermissionState.eventSink(PermissionsEvent.RequestPermissions)
|
||||
}
|
||||
localNetworkPromptDismissedThisSession = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,6 +180,7 @@ class LoggedInPresenter(
|
||||
ignoreRegistrationError = ignoreRegistrationError,
|
||||
forceNativeSlidingSyncMigration = forceNativeSlidingSyncMigration,
|
||||
appName = buildMeta.applicationName,
|
||||
localNetworkPermissionDialog = localNetworkPermissionDialog,
|
||||
eventSink = ::handleEvent,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
package io.element.android.appnav.loggedin
|
||||
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionDialog
|
||||
|
||||
data class LoggedInState(
|
||||
val showSyncSpinner: Boolean,
|
||||
@@ -16,5 +17,6 @@ data class LoggedInState(
|
||||
val ignoreRegistrationError: Boolean,
|
||||
val forceNativeSlidingSyncMigration: Boolean,
|
||||
val appName: String,
|
||||
val localNetworkPermissionDialog: LocalNetworkPermissionDialog,
|
||||
val eventSink: (LoggedInEvents) -> Unit,
|
||||
)
|
||||
|
||||
@@ -10,6 +10,7 @@ package io.element.android.appnav.loggedin
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionDialog
|
||||
import io.element.android.libraries.push.api.PusherRegistrationFailure
|
||||
|
||||
open class LoggedInStateProvider : PreviewParameterProvider<LoggedInState> {
|
||||
@@ -19,6 +20,8 @@ open class LoggedInStateProvider : PreviewParameterProvider<LoggedInState> {
|
||||
aLoggedInState(showSyncSpinner = true),
|
||||
aLoggedInState(pusherRegistrationState = AsyncData.Failure(PusherRegistrationFailure.NoDistributorsAvailable())),
|
||||
aLoggedInState(forceNativeSlidingSyncMigration = true),
|
||||
aLoggedInState(localNetworkPermissionDialog = LocalNetworkPermissionDialog.Rationale),
|
||||
aLoggedInState(localNetworkPermissionDialog = LocalNetworkPermissionDialog.Settings),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -27,11 +30,13 @@ fun aLoggedInState(
|
||||
pusherRegistrationState: AsyncData<Unit> = AsyncData.Uninitialized,
|
||||
forceNativeSlidingSyncMigration: Boolean = false,
|
||||
appName: String = "Element X",
|
||||
localNetworkPermissionDialog: LocalNetworkPermissionDialog = LocalNetworkPermissionDialog.None,
|
||||
) = LoggedInState(
|
||||
showSyncSpinner = showSyncSpinner,
|
||||
pusherRegistrationState = pusherRegistrationState,
|
||||
ignoreRegistrationError = false,
|
||||
forceNativeSlidingSyncMigration = forceNativeSlidingSyncMigration,
|
||||
appName = appName,
|
||||
localNetworkPermissionDialog = localNetworkPermissionDialog,
|
||||
eventSink = {},
|
||||
)
|
||||
|
||||
@@ -25,6 +25,7 @@ import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.utils.OnLifecycleEvent
|
||||
import io.element.android.libraries.matrix.api.exception.isNetworkError
|
||||
import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionDialogView
|
||||
import io.element.android.libraries.push.api.PusherRegistrationFailure
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
|
||||
@@ -82,6 +83,12 @@ fun LoggedInView(
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
LocalNetworkPermissionDialogView(
|
||||
dialog = state.localNetworkPermissionDialog,
|
||||
onSubmit = { state.eventSink(LoggedInEvents.RequestLocationNetworkPermission) },
|
||||
onDismiss = { state.eventSink(LoggedInEvents.DismissLocalNetworkPermissionPrompt) },
|
||||
)
|
||||
}
|
||||
|
||||
private fun Throwable.getReason(): String? {
|
||||
|
||||
@@ -36,6 +36,8 @@ import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService
|
||||
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
|
||||
import io.element.android.libraries.matrix.test.sync.FakeSyncService
|
||||
import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService
|
||||
import io.element.android.libraries.permissions.test.FakeLocalNetworkPermissionAdvisor
|
||||
import io.element.android.libraries.permissions.test.FakePermissionsPresenterFactory
|
||||
import io.element.android.libraries.push.api.PushService
|
||||
import io.element.android.libraries.push.api.PusherRegistrationFailure
|
||||
import io.element.android.libraries.push.test.FakePushService
|
||||
@@ -124,6 +126,8 @@ class LoggedInPresenterTest {
|
||||
encryptionService = encryptionService,
|
||||
buildMeta = buildMeta,
|
||||
networkMonitor = networkMonitor,
|
||||
localNetworkPermissionAdvisor = FakeLocalNetworkPermissionAdvisor(),
|
||||
permissionsPresenterFactory = FakePermissionsPresenterFactory(),
|
||||
).test {
|
||||
encryptionService.emitRecoveryState(RecoveryState.UNKNOWN)
|
||||
encryptionService.emitRecoveryState(RecoveryState.INCOMPLETE)
|
||||
@@ -368,6 +372,8 @@ class LoggedInPresenterTest {
|
||||
encryptionService = encryptionService,
|
||||
buildMeta = buildMeta,
|
||||
networkMonitor = networkMonitor,
|
||||
localNetworkPermissionAdvisor = FakeLocalNetworkPermissionAdvisor(),
|
||||
permissionsPresenterFactory = FakePermissionsPresenterFactory(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
Submodule enterprise updated: efdf9d9a15...cca96c4fc2
@@ -0,0 +1,2 @@
|
||||
Main changes in this version: bug fixes and improvements, including fixing the local network permission usage in Android 17.
|
||||
Full changelog: https://github.com/element-hq/element-x-android/releases
|
||||
+6
-7
@@ -31,12 +31,12 @@ import io.element.android.compound.tokens.generated.CompoundIcons
|
||||
import io.element.android.features.analytics.api.AnalyticsOptInEvents
|
||||
import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule
|
||||
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
|
||||
import io.element.android.libraries.designsystem.atomic.organisms.InfoListItem
|
||||
import io.element.android.libraries.designsystem.atomic.organisms.InfoListOrganism
|
||||
import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
|
||||
import io.element.android.libraries.designsystem.background.OnboardingBackground
|
||||
import io.element.android.libraries.designsystem.components.BigIcon
|
||||
import io.element.android.libraries.designsystem.components.ClickableLinkText
|
||||
import io.element.android.libraries.designsystem.components.visuallist.VisualList
|
||||
import io.element.android.libraries.designsystem.components.visuallist.VisualListItemData
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.text.buildAnnotatedStringWithStyledPart
|
||||
@@ -129,22 +129,21 @@ private fun AnalyticsOptInContent() {
|
||||
verticalBias = -0.4f
|
||||
)
|
||||
) {
|
||||
InfoListOrganism(
|
||||
VisualList(
|
||||
items = persistentListOf(
|
||||
InfoListItem(
|
||||
VisualListItemData(
|
||||
message = stringResource(id = R.string.screen_analytics_prompt_data_usage),
|
||||
iconVector = CompoundIcons.CheckCircle(),
|
||||
),
|
||||
InfoListItem(
|
||||
VisualListItemData(
|
||||
message = stringResource(id = R.string.screen_analytics_prompt_third_party_sharing),
|
||||
iconVector = CompoundIcons.CheckCircle(),
|
||||
),
|
||||
InfoListItem(
|
||||
VisualListItemData(
|
||||
message = stringResource(id = R.string.screen_analytics_prompt_settings),
|
||||
iconVector = CompoundIcons.CheckCircle(),
|
||||
),
|
||||
),
|
||||
textStyle = ElementTheme.typography.fontBodyLgMedium,
|
||||
iconTint = ElementTheme.colors.iconSuccessPrimary,
|
||||
)
|
||||
}
|
||||
|
||||
+9
-11
@@ -36,10 +36,10 @@ import io.element.android.features.announcement.impl.AnnouncementState
|
||||
import io.element.android.features.announcement.impl.AnnouncementStateProvider
|
||||
import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule
|
||||
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
|
||||
import io.element.android.libraries.designsystem.atomic.organisms.InfoListItem
|
||||
import io.element.android.libraries.designsystem.atomic.organisms.InfoListOrganism
|
||||
import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
|
||||
import io.element.android.libraries.designsystem.components.BigIcon
|
||||
import io.element.android.libraries.designsystem.components.visuallist.VisualList
|
||||
import io.element.android.libraries.designsystem.components.visuallist.VisualListItemData
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.theme.components.Button
|
||||
@@ -134,11 +134,9 @@ private fun FullscreenAnnouncementContent(
|
||||
Column(
|
||||
modifier = modifier.fillMaxSize(),
|
||||
) {
|
||||
InfoListOrganism(
|
||||
VisualList(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
items = announcement.items(),
|
||||
textStyle = ElementTheme.typography.fontBodyLgMedium,
|
||||
iconTint = ElementTheme.colors.iconSecondary,
|
||||
iconSize = 24.dp
|
||||
)
|
||||
announcement.notice()?.let { notice ->
|
||||
@@ -186,25 +184,25 @@ private fun Announcement.Fullscreen.icon() = when (this) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Announcement.Fullscreen.items(): ImmutableList<InfoListItem> = when (this) {
|
||||
private fun Announcement.Fullscreen.items(): ImmutableList<VisualListItemData> = when (this) {
|
||||
Announcement.Fullscreen.Space -> persistentListOf(
|
||||
InfoListItem(
|
||||
VisualListItemData(
|
||||
message = "View spaces you\'ve created or joined",
|
||||
iconVector = CompoundIcons.VisibilityOn(),
|
||||
),
|
||||
InfoListItem(
|
||||
VisualListItemData(
|
||||
message = "Accept or decline invites to spaces",
|
||||
iconVector = CompoundIcons.Email(),
|
||||
),
|
||||
InfoListItem(
|
||||
VisualListItemData(
|
||||
message = "Discover any rooms you can join in your spaces",
|
||||
iconVector = CompoundIcons.Search(),
|
||||
),
|
||||
InfoListItem(
|
||||
VisualListItemData(
|
||||
message = "Join public spaces",
|
||||
iconVector = CompoundIcons.Explore(),
|
||||
),
|
||||
InfoListItem(
|
||||
VisualListItemData(
|
||||
message = "Leave any spaces you’ve joined",
|
||||
iconVector = CompoundIcons.Leave(),
|
||||
),
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
android:name="android.hardware.microphone"
|
||||
android:required="false" />
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
|
||||
+1
-2
@@ -22,7 +22,6 @@ import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.AssistedFactory
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import im.vector.app.features.analytics.plan.MobileScreen
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.features.call.api.CallData
|
||||
import io.element.android.features.call.impl.data.WidgetMessage
|
||||
import io.element.android.features.call.impl.utils.ActiveCallManager
|
||||
@@ -84,7 +83,7 @@ class CallScreenPresenter(
|
||||
var ignoreWebViewError by rememberSaveable { mutableStateOf(false) }
|
||||
var webViewError by remember { mutableStateOf<String?>(null) }
|
||||
val languageTag = languageTagProvider.provideLanguageTag()
|
||||
val theme = if (ElementTheme.isLightTheme) "light" else "dark"
|
||||
val theme = "dark"
|
||||
|
||||
DisposableEffect(Unit) {
|
||||
coroutineScope.launch {
|
||||
|
||||
+21
-16
@@ -38,6 +38,7 @@ import androidx.core.view.WindowInsetsControllerCompat
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import dev.zacsweers.metro.Inject
|
||||
import io.element.android.compound.colors.SemanticColorsLightDark
|
||||
import io.element.android.compound.theme.ForcedDarkElementTheme
|
||||
import io.element.android.features.call.api.CallData
|
||||
import io.element.android.features.call.impl.DefaultElementCallEntryPoint
|
||||
import io.element.android.features.call.impl.di.CallBindings
|
||||
@@ -143,24 +144,28 @@ class ElementCallActivity :
|
||||
compoundDark = colors.dark,
|
||||
buildMeta = buildMeta,
|
||||
) {
|
||||
val state = presenter.present()
|
||||
eventSink = state.eventSink
|
||||
LaunchedEffect(state.isCallActive) {
|
||||
if (state.isCallActive) {
|
||||
setCallIsActive()
|
||||
ForcedDarkElementTheme(
|
||||
colors = colors,
|
||||
) {
|
||||
val state = presenter.present()
|
||||
eventSink = state.eventSink
|
||||
LaunchedEffect(state.isCallActive) {
|
||||
if (state.isCallActive) {
|
||||
setCallIsActive()
|
||||
}
|
||||
}
|
||||
CallScreenView(
|
||||
state = state,
|
||||
pipState = pipState,
|
||||
onConsoleMessage = {
|
||||
consoleMessageLogger.log("ElementCall", it)
|
||||
},
|
||||
requestPermissions = { permissions, callback ->
|
||||
requestPermissionCallback = callback
|
||||
requestPermissionsLauncher.launch(permissions)
|
||||
}
|
||||
)
|
||||
}
|
||||
CallScreenView(
|
||||
state = state,
|
||||
pipState = pipState,
|
||||
onConsoleMessage = {
|
||||
consoleMessageLogger.log("ElementCall", it)
|
||||
},
|
||||
requestPermissions = { permissions, callback ->
|
||||
requestPermissionCallback = callback
|
||||
requestPermissionsLauncher.launch(permissions)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
plugins {
|
||||
id("io.element.android-compose-library")
|
||||
id("kotlin-parcelize")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "io.element.android.features.contentscanner.api"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.libraries.core)
|
||||
implementation(projects.libraries.architecture)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
implementation(projects.libraries.matrixui)
|
||||
implementation(projects.libraries.designsystem)
|
||||
|
||||
testImplementation(libs.test.junit)
|
||||
testImplementation(libs.coroutines.test)
|
||||
testImplementation(libs.molecule.runtime)
|
||||
testImplementation(libs.test.truth)
|
||||
testImplementation(libs.test.turbine)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import extension.setupDependencyInjection
|
||||
|
||||
plugins {
|
||||
id("io.element.android-compose-library")
|
||||
id("kotlin-parcelize")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "io.element.android.features.contentscanner.impl"
|
||||
}
|
||||
|
||||
setupDependencyInjection()
|
||||
|
||||
dependencies {
|
||||
api(projects.features.contentscanner.api)
|
||||
implementation(projects.libraries.core)
|
||||
implementation(projects.libraries.architecture)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
implementation(projects.libraries.matrixui)
|
||||
implementation(projects.libraries.designsystem)
|
||||
|
||||
testImplementation(libs.test.junit)
|
||||
testImplementation(libs.coroutines.test)
|
||||
testImplementation(libs.molecule.runtime)
|
||||
testImplementation(libs.test.truth)
|
||||
testImplementation(libs.test.turbine)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
}
|
||||
@@ -43,6 +43,7 @@ dependencies {
|
||||
implementation(libs.coil.compose)
|
||||
implementation(projects.libraries.featureflag.api)
|
||||
implementation(projects.features.invitepeople.api)
|
||||
implementation(projects.features.enterprise.api)
|
||||
api(projects.features.createroom.api)
|
||||
|
||||
testCommonDependencies(libs, true)
|
||||
@@ -53,4 +54,5 @@ dependencies {
|
||||
testImplementation(projects.libraries.permissions.test)
|
||||
testImplementation(projects.features.startchat.test)
|
||||
testImplementation(projects.libraries.featureflag.test)
|
||||
testImplementation(projects.features.enterprise.test)
|
||||
}
|
||||
|
||||
+3
-1
@@ -24,6 +24,7 @@ import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.AssistedFactory
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import im.vector.app.features.analytics.plan.CreatedRoom
|
||||
import io.element.android.features.enterprise.api.SessionEnterpriseService
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.architecture.runCatchingUpdatingState
|
||||
@@ -73,6 +74,7 @@ class ConfigureRoomPresenter(
|
||||
private val featureFlagService: FeatureFlagService,
|
||||
private val roomAliasHelper: RoomAliasHelper,
|
||||
private val mediaOptimizationConfigProvider: MediaOptimizationConfigProvider,
|
||||
private val sessionEnterpriseService: SessionEnterpriseService,
|
||||
) : Presenter<ConfigureRoomState> {
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
@@ -256,7 +258,7 @@ class ConfigureRoomPresenter(
|
||||
CreateRoomParameters(
|
||||
name = config.roomName,
|
||||
topic = config.topic,
|
||||
isEncrypted = true,
|
||||
isEncrypted = !sessionEnterpriseService.isEncryptionDisabledByHomeserver(),
|
||||
isDirect = false,
|
||||
visibility = RoomVisibility.Private,
|
||||
historyVisibilityOverride = RoomHistoryVisibility.Invited,
|
||||
|
||||
+5
-4
@@ -11,7 +11,8 @@ import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.material3.SheetValue
|
||||
import androidx.compose.material3.rememberBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
@@ -85,9 +86,9 @@ internal fun SelectParentSpaceOptions(
|
||||
)
|
||||
|
||||
if (displaySelectSpaceBottomSheet) {
|
||||
val sheetState = rememberModalBottomSheetState(
|
||||
skipPartiallyExpanded = true,
|
||||
confirmValueChange = { true },
|
||||
val sheetState = rememberBottomSheetState(
|
||||
initialValue = SheetValue.Hidden,
|
||||
enabledValues = setOf(SheetValue.Hidden, SheetValue.Expanded),
|
||||
)
|
||||
ModalBottomSheet(
|
||||
sheetState = sheetState,
|
||||
|
||||
@@ -3,14 +3,34 @@
|
||||
<string name="screen_create_room_action_create_room">"Ruangan baru"</string>
|
||||
<string name="screen_create_room_add_people_title">"Undang orang-orang"</string>
|
||||
<string name="screen_create_room_error_creating_room">"Terjadi kesalahan saat membuat ruangan"</string>
|
||||
<string name="screen_create_room_error_creating_space">"Ruang tersebut tidak dapat dibuat karena kesalahan yang tidak diketahui. Coba lagi nanti."</string>
|
||||
<string name="screen_create_room_name_placeholder">"Tambahkan nama…"</string>
|
||||
<string name="screen_create_room_new_room_title">"Ruangan baru"</string>
|
||||
<string name="screen_create_room_new_space_title">"Ruang baru"</string>
|
||||
<string name="screen_create_room_private_option_description">"Hanya orang-orang yang diundang dapat mengakses ruangan ini. Semua pesan terenkripsi secara ujung ke ujung."</string>
|
||||
<string name="screen_create_room_private_option_title">"Pribadi"</string>
|
||||
<string name="screen_create_room_public_option_description">"Siapa pun dapat mencari ruangan ini.
|
||||
Anda dapat mengubah ini kapan pun dalam pengaturan ruangan."</string>
|
||||
<string name="screen_create_room_public_option_short_description">"Siapa pun bisa bergabung."</string>
|
||||
<string name="screen_create_room_public_option_title">"Umum"</string>
|
||||
<string name="screen_create_room_room_access_section_knocking_option_description">"Siapa pun dapat meminta untuk bergabung dengan ijin administrator atau moderator"</string>
|
||||
<string name="screen_create_room_room_access_section_knocking_option_title">"Minta untuk bergabung"</string>
|
||||
<string name="screen_create_room_room_access_section_knocking_restricted_option_description">"Siapa pun di %1$s dapat bergabung, tetapi semua orang lain harus meminta akses."</string>
|
||||
<string name="screen_create_room_room_access_section_knocking_restricted_option_title">"Ajukan permohonan untuk bergabung"</string>
|
||||
<string name="screen_create_room_room_access_section_private_option_description">"Hanya orang-orang yang diundang dapat mengakses ruangan ini. Semua pesan terenkripsi secara ujung ke ujung."</string>
|
||||
<string name="screen_create_room_room_access_section_private_option_title">"Pribadi"</string>
|
||||
<string name="screen_create_room_room_access_section_public_option_description">"Siapa pun dapat bergabung dengan ruangan ini"</string>
|
||||
<string name="screen_create_room_room_access_section_public_option_title">"Umum"</string>
|
||||
<string name="screen_create_room_room_access_section_restricted_option_description">"Siapa pun di %1$s dapat bergabung."</string>
|
||||
<string name="screen_create_room_room_access_section_restricted_option_title">"Standar"</string>
|
||||
<string name="screen_create_room_room_access_section_title">"Yang memiliki akses"</string>
|
||||
<string name="screen_create_room_room_address_section_footer">"Supaya ruangan ini terlihat di direktori ruangan publik, Anda memerlukan alamat ruangan."</string>
|
||||
<string name="screen_create_room_room_address_section_title">"Alamat ruangan"</string>
|
||||
<string name="screen_create_room_room_visibility_section_title">"Keterlihatan ruangan"</string>
|
||||
<string name="screen_create_room_space_selection_no_space_description">"(ruang sudah penuh)"</string>
|
||||
<string name="screen_create_room_space_selection_no_space_option">"Jangan menambahkan sesuatu ke ruang yang sudah ada."</string>
|
||||
<string name="screen_create_room_space_selection_no_space_title">"Tidak ada ruang yang dipilih"</string>
|
||||
<string name="screen_create_room_space_selection_sheet_title">"Tambahkan ke ruang"</string>
|
||||
<string name="screen_create_room_topic_label">"Topik (opsional)"</string>
|
||||
<string name="screen_create_room_topic_placeholder">"Tambahkan deskripsi…"</string>
|
||||
</resources>
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<string name="screen_create_room_room_access_section_public_option_title">"公開"</string>
|
||||
<string name="screen_create_room_room_access_section_restricted_option_description">"%1$s にいる全員が参加することができます。"</string>
|
||||
<string name="screen_create_room_room_access_section_restricted_option_title">"スタンダード"</string>
|
||||
<string name="screen_create_room_room_access_section_title">"参加できるユーザー"</string>
|
||||
<string name="screen_create_room_room_access_section_title">"参加可能なユーザー"</string>
|
||||
<string name="screen_create_room_room_address_section_footer">"公開ディレクトリで自分を見つけられるようにするには、アドレスが必要です。"</string>
|
||||
<string name="screen_create_room_room_address_section_title">"アドレス"</string>
|
||||
<string name="screen_create_room_room_visibility_section_title">"ルームの公開度"</string>
|
||||
|
||||
+29
@@ -20,11 +20,13 @@ import io.element.android.features.createroom.impl.configureroom.CreateRoomConfi
|
||||
import io.element.android.features.createroom.impl.configureroom.JoinRuleItem
|
||||
import io.element.android.features.createroom.impl.configureroom.RoomAddress
|
||||
import io.element.android.features.createroom.impl.configureroom.RoomVisibilityState
|
||||
import io.element.android.features.enterprise.test.FakeSessionEnterpriseService
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.createroom.CreateRoomParameters
|
||||
import io.element.android.libraries.matrix.api.room.RoomInfo
|
||||
import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias
|
||||
import io.element.android.libraries.matrix.api.room.alias.RoomAliasHelper
|
||||
@@ -56,6 +58,7 @@ import io.element.android.services.analytics.api.AnalyticsService
|
||||
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||
import io.element.android.tests.testutils.WarmUpRule
|
||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||
import io.element.android.tests.testutils.lambda.matching
|
||||
import io.element.android.tests.testutils.robolectric.RobolectricTest
|
||||
import io.element.android.tests.testutils.test
|
||||
import io.mockk.mockk
|
||||
@@ -517,6 +520,30 @@ class ConfigureRoomPresenterTest : RobolectricTest() {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - creating a private room when encryption is disabled by the HS creates it unencrypted`() = runTest {
|
||||
val sessionEnterpriseService = FakeSessionEnterpriseService(isEncryptionDisabledResult = { true })
|
||||
val createRoomLambda = lambdaRecorder<CreateRoomParameters, Result<RoomId>> { Result.success(A_ROOM_ID) }
|
||||
val matrixClient = createMatrixClient().apply {
|
||||
createRoomResult = createRoomLambda
|
||||
}
|
||||
val presenter = createConfigureRoomPresenter(
|
||||
matrixClient = matrixClient,
|
||||
sessionEnterpriseService = sessionEnterpriseService,
|
||||
)
|
||||
presenter.test {
|
||||
val initialState = initialState()
|
||||
|
||||
initialState.eventSink(ConfigureRoomEvents.JoinRuleChanged(JoinRuleItem.PrivateVisibility.Private))
|
||||
initialState.eventSink(ConfigureRoomEvents.CreateRoom)
|
||||
|
||||
assertThat(awaitItem().createRoomAction.isLoading()).isTrue()
|
||||
assertThat(awaitItem().createRoomAction.isSuccess()).isTrue()
|
||||
|
||||
createRoomLambda.assertions().isCalledOnce().with(matching<CreateRoomParameters> { !it.isEncrypted })
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun TurbineTestContext<ConfigureRoomState>.initialState(): ConfigureRoomState {
|
||||
skipItems(1)
|
||||
return awaitItem()
|
||||
@@ -552,6 +579,7 @@ class ConfigureRoomPresenterTest : RobolectricTest() {
|
||||
permissionsPresenter: PermissionsPresenter = FakePermissionsPresenter(),
|
||||
isKnockFeatureEnabled: Boolean = true,
|
||||
mediaOptimizationConfigProvider: FakeMediaOptimizationConfigProvider = FakeMediaOptimizationConfigProvider(),
|
||||
sessionEnterpriseService: FakeSessionEnterpriseService = FakeSessionEnterpriseService(isEncryptionDisabledResult = { false }),
|
||||
) = ConfigureRoomPresenter(
|
||||
isSpace = isSpace,
|
||||
initialParentSpaceId = initialParenSpaceId,
|
||||
@@ -566,5 +594,6 @@ class ConfigureRoomPresenterTest : RobolectricTest() {
|
||||
mapOf(FeatureFlags.Knock.key to isKnockFeatureEnabled)
|
||||
),
|
||||
mediaOptimizationConfigProvider = mediaOptimizationConfigProvider,
|
||||
sessionEnterpriseService = sessionEnterpriseService,
|
||||
)
|
||||
}
|
||||
|
||||
+7
-7
@@ -48,11 +48,11 @@ import io.element.android.compound.tokens.generated.CompoundIcons
|
||||
import io.element.android.features.deactivation.impl.R
|
||||
import io.element.android.features.logout.impl.ui.AccountDeactivationActionDialog
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.designsystem.atomic.organisms.InfoListItem
|
||||
import io.element.android.libraries.designsystem.atomic.organisms.InfoListOrganism
|
||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||
import io.element.android.libraries.designsystem.components.form.textFieldState
|
||||
import io.element.android.libraries.designsystem.components.list.SwitchListItem
|
||||
import io.element.android.libraries.designsystem.components.visuallist.VisualList
|
||||
import io.element.android.libraries.designsystem.components.visuallist.VisualListItemData
|
||||
import io.element.android.libraries.designsystem.modifiers.onTabOrEnterKeyFocusNext
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
@@ -175,9 +175,9 @@ private fun Content(
|
||||
style = ElementTheme.typography.fontBodyMdRegular,
|
||||
color = ElementTheme.colors.textSecondary,
|
||||
)
|
||||
InfoListOrganism(
|
||||
VisualList(
|
||||
items = persistentListOf(
|
||||
InfoListItem(
|
||||
VisualListItemData(
|
||||
message = buildAnnotatedStringWithStyledPart(
|
||||
R.string.screen_deactivate_account_list_item_1,
|
||||
R.string.screen_deactivate_account_list_item_1_bold_part,
|
||||
@@ -194,7 +194,7 @@ private fun Content(
|
||||
)
|
||||
},
|
||||
),
|
||||
InfoListItem(
|
||||
VisualListItemData(
|
||||
message = stringResource(R.string.screen_deactivate_account_list_item_2),
|
||||
iconComposable = {
|
||||
Icon(
|
||||
@@ -205,7 +205,7 @@ private fun Content(
|
||||
)
|
||||
},
|
||||
),
|
||||
InfoListItem(
|
||||
VisualListItemData(
|
||||
message = stringResource(R.string.screen_deactivate_account_list_item_3),
|
||||
iconComposable = {
|
||||
Icon(
|
||||
@@ -216,7 +216,7 @@ private fun Content(
|
||||
)
|
||||
},
|
||||
),
|
||||
InfoListItem(
|
||||
VisualListItemData(
|
||||
message = stringResource(R.string.screen_deactivate_account_list_item_4),
|
||||
iconComposable = {
|
||||
Icon(
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_deactivate_account_confirmation_dialog_content">"Bekreft at du vil deaktivere kontoen din. Denne handlingen kan ikke angres."</string>
|
||||
<string name="screen_deactivate_account_confirmation_dialog_content">"Bekreft at du vil slette kontoen din. Denne handlingen kan ikke angres."</string>
|
||||
<string name="screen_deactivate_account_delete_all_messages">"Slett alle meldingene mine"</string>
|
||||
<string name="screen_deactivate_account_delete_all_messages_notice">"Advarsel: Fremtidige brukere vil kunne se ufullstendige samtaler."</string>
|
||||
<string name="screen_deactivate_account_description">"Deaktivering av kontoen din er %1$s , det vil:"</string>
|
||||
<string name="screen_deactivate_account_description">"Hvis %1$s sletter kontoen din, vil følgende skje:"</string>
|
||||
<string name="screen_deactivate_account_description_bold_part">"irreversibel"</string>
|
||||
<string name="screen_deactivate_account_list_item_1">"%1$s kontoen din (du kan ikke logge på igjen, og ID-en din kan ikke brukes på nytt)."</string>
|
||||
<string name="screen_deactivate_account_list_item_1_bold_part">"Deaktiver permanent"</string>
|
||||
<string name="screen_deactivate_account_list_item_2">"Fjern deg fra alle chatterom."</string>
|
||||
<string name="screen_deactivate_account_list_item_3">"Slett kontoinformasjonen din fra vår identitetsserver."</string>
|
||||
<string name="screen_deactivate_account_list_item_4">"Meldingene dine vil fortsatt være synlige for registrerte brukere, men vil ikke være tilgjengelige for nye eller uregistrerte brukere hvis du velger å slette dem."</string>
|
||||
<string name="screen_deactivate_account_title">"Deaktiver kontoen"</string>
|
||||
<string name="screen_deactivate_account_title">"Slett kontoen"</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_deactivate_account_confirmation_dialog_content">"Iltimos, hisobingizni o‘chirishni xohlayotganingizni tasdiqlang. Bu amalni qaytarib bo‘lmaydi."</string>
|
||||
<string name="screen_deactivate_account_confirmation_dialog_content">"Iltimos, hisobingizni oʻchirishni xohlayotganingizni tasdiqlang. Bu amalni qaytarib boʻlmaydi."</string>
|
||||
<string name="screen_deactivate_account_delete_all_messages">"Barcha xabarlarimni o‘chirib tashlang"</string>
|
||||
<string name="screen_deactivate_account_delete_all_messages_notice">"Ogohlantirish: Kelgusi foydalanuvchilar chala suhbatlarni ko‘rishi mumkin."</string>
|
||||
<string name="screen_deactivate_account_description">"Hisobingiz %1$s faolsizlantirilmoqda, u quyidagilarni bajaradi:"</string>
|
||||
|
||||
+2
@@ -12,5 +12,7 @@ interface SessionEnterpriseService {
|
||||
suspend fun isElementCallAvailable(): Boolean
|
||||
suspend fun tweakMasUrl(url: String): String
|
||||
|
||||
suspend fun isEncryptionDisabledByHomeserver(): Boolean
|
||||
|
||||
suspend fun init()
|
||||
}
|
||||
|
||||
+1
@@ -17,4 +17,5 @@ class DefaultSessionEnterpriseService : SessionEnterpriseService {
|
||||
override suspend fun init() = Unit
|
||||
override suspend fun tweakMasUrl(url: String): String = url
|
||||
override suspend fun isElementCallAvailable(): Boolean = true
|
||||
override suspend fun isEncryptionDisabledByHomeserver(): Boolean = false
|
||||
}
|
||||
|
||||
+5
@@ -15,6 +15,7 @@ import io.element.android.tests.testutils.simulateLongTask
|
||||
class FakeSessionEnterpriseService(
|
||||
private val isElementCallAvailableResult: () -> Boolean = { lambdaError() },
|
||||
private val tweakMasUrlResult: (String) -> String = { lambdaError() },
|
||||
private val isEncryptionDisabledResult: () -> Boolean = { lambdaError() },
|
||||
) : SessionEnterpriseService {
|
||||
override suspend fun init() {
|
||||
}
|
||||
@@ -26,4 +27,8 @@ class FakeSessionEnterpriseService(
|
||||
override suspend fun isElementCallAvailable(): Boolean = simulateLongTask {
|
||||
isElementCallAvailableResult()
|
||||
}
|
||||
|
||||
override suspend fun isEncryptionDisabledByHomeserver(): Boolean {
|
||||
return isEncryptionDisabledResult()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_identity_confirmation_cannot_confirm">"نمیتوانید تأیید کنید؟"</string>
|
||||
<string name="screen_identity_confirmation_create_new_recovery_key">"ایجاد کلید بازیابی جدید"</string>
|
||||
<string name="screen_identity_confirmation_subtitle">"تأیید این افزاره برای برپایی پیامرسانی امن."</string>
|
||||
<string name="screen_identity_confirmation_title">"تأیید هویتتان"</string>
|
||||
<string name="screen_identity_confirmation_subtitle">"گزینش چگونگی تأیید برای برپایی پیامرسانی امن."</string>
|
||||
<string name="screen_identity_confirmation_title">"تأیید هویت دیجیتالیتان"</string>
|
||||
<string name="screen_identity_confirmation_use_another_device">"استفاده از افزارهای دیگر"</string>
|
||||
<string name="screen_identity_confirmation_use_recovery_key">"استفاده از کلید بازیابی"</string>
|
||||
<string name="screen_identity_confirmed_subtitle">"اکنون میتوانید پیامها را به صورت امن فرستاده و بگیرید و هرکسی که با او گپ میزنید نیز میتواند به این افزاره اعتماد کند."</string>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_identity_confirmation_cannot_confirm">"Kan du ikke bekrefte?"</string>
|
||||
<string name="screen_identity_confirmation_create_new_recovery_key">"Opprett en ny gjenopprettingsnøkkel"</string>
|
||||
<string name="screen_identity_confirmation_subtitle">"Verifiser denne enheten for å sette opp sikker meldingsutveksling."</string>
|
||||
<string name="screen_identity_confirmation_subtitle">"Velg hvordan du vil verifisere deg for å sette opp sikker meldingsutveksling."</string>
|
||||
<string name="screen_identity_confirmation_title">"Bekreft din digitale identitet"</string>
|
||||
<string name="screen_identity_confirmation_use_another_device">"Bruk en annen enhet"</string>
|
||||
<string name="screen_identity_confirmation_use_recovery_key">"Bruk gjenopprettingsnøkkel"</string>
|
||||
|
||||
+2
-2
@@ -106,7 +106,7 @@ private fun RoomListModalBottomSheetContent(
|
||||
ListItem(
|
||||
headlineContent = {
|
||||
Text(
|
||||
text = stringResource(id = R.string.screen_roomlist_mark_as_read),
|
||||
text = stringResource(id = CommonStrings.action_mark_as_read),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
)
|
||||
},
|
||||
@@ -200,7 +200,7 @@ private fun RoomListModalBottomSheetContent(
|
||||
@Composable
|
||||
internal fun RoomListContextMenuPreview(
|
||||
@PreviewParameter(RoomListStateContextMenuShownProvider::class) contextMenu: RoomListState.ContextMenu.Shown
|
||||
) = ElementPreview {
|
||||
) = ElementPreview(fillMaxSize = true) {
|
||||
RoomListContextMenu(
|
||||
contextMenu = contextMenu,
|
||||
canReportRoom = true,
|
||||
|
||||
+6
-2
@@ -33,6 +33,8 @@ import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.contentDescription
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||
@@ -112,10 +114,12 @@ private fun RoomListSearchContent(
|
||||
title = {
|
||||
// The stateSaver will keep the selection state when returning to this UI
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
val searchLabel = stringResource(CommonStrings.action_search)
|
||||
FilledTextField(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.focusRequester(focusRequester),
|
||||
.focusRequester(focusRequester)
|
||||
.semantics { contentDescription = searchLabel },
|
||||
state = state.query,
|
||||
lineLimits = TextFieldLineLimits.SingleLine,
|
||||
colors = TextFieldDefaults.colors(
|
||||
@@ -132,7 +136,7 @@ private fun RoomListSearchContent(
|
||||
IconButton(onClick = { state.eventSink(RoomListSearchEvent.ClearQuery) }) {
|
||||
Icon(
|
||||
imageVector = CompoundIcons.Close(),
|
||||
contentDescription = stringResource(CommonStrings.action_cancel)
|
||||
contentDescription = stringResource(CommonStrings.a11y_clear_search_field)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+5
-4
@@ -24,7 +24,7 @@ import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.text.input.TextFieldState
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.SheetValue
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.material3.rememberBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
@@ -57,8 +57,9 @@ fun SpaceFiltersView(
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val isSelecting by rememberUpdatedState(state is SpaceFiltersState.Selecting)
|
||||
val sheetState = rememberModalBottomSheetState(
|
||||
skipPartiallyExpanded = true,
|
||||
val sheetState = rememberBottomSheetState(
|
||||
initialValue = SheetValue.Hidden,
|
||||
enabledValues = setOf(SheetValue.Hidden, SheetValue.Expanded),
|
||||
confirmValueChange = { sheetValueTarget ->
|
||||
// This ensures the hide animation is not cancelled
|
||||
when (sheetValueTarget) {
|
||||
@@ -187,6 +188,6 @@ private fun SpaceFilterItem(
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun SpaceFiltersViewPreview(@PreviewParameter(SpaceFiltersStateProvider::class) state: SpaceFiltersState) = ElementPreview {
|
||||
internal fun SpaceFiltersViewPreview(@PreviewParameter(SpaceFiltersStateProvider::class) state: SpaceFiltersState) = ElementPreview(fillMaxSize = true) {
|
||||
SpaceFiltersView(state = state)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
<string name="banner_battery_optimization_content_android">"Nonaktifkan pengoptimalan baterai untuk aplikasi ini, untuk memastikan semua notifikasi diterima."</string>
|
||||
<string name="banner_battery_optimization_submit_android">"Nonaktifkan optimasi"</string>
|
||||
<string name="banner_battery_optimization_title_android">"Notifikasi tidak masuk?"</string>
|
||||
<string name="banner_new_sound_message">"Pemberitahuan Anda telah diperbarui—lebih jelas, lebih cepat, dan tidak mengganggu."</string>
|
||||
<string name="banner_new_sound_title">"Kami telah memperbarui suara Anda."</string>
|
||||
<string name="banner_set_up_recovery_content">"Buat kunci pemulihan baru yang dapat digunakan untuk memulihkan riwayat pesan terenkripsi Anda jika Anda kehilangan akses ke perangkat Anda."</string>
|
||||
<string name="banner_set_up_recovery_submit">"Siapkan Kunci pemulihan"</string>
|
||||
<string name="banner_set_up_recovery_title">"Cadangkan obrolan Anda"</string>
|
||||
@@ -48,6 +50,7 @@ Anda tidak memiliki pesan yang belum dibaca!"</string>
|
||||
<string name="screen_roomlist_mark_as_read">"Tandai sebagai dibaca"</string>
|
||||
<string name="screen_roomlist_mark_as_unread">"Tandai sebagai belum dibaca"</string>
|
||||
<string name="screen_roomlist_tombstoned_room_description">"Ruangan ini telah ditingkatkan"</string>
|
||||
<string name="screen_roomlist_your_spaces">"Ruang Anda"</string>
|
||||
<string name="session_verification_banner_message">"Sepertinya Anda menggunakan perangkat baru. Verifikasi dengan perangkat lain untuk mengakses pesan terenkripsi Anda selanjutnya."</string>
|
||||
<string name="session_verification_banner_title">"Verifikasi bahwa ini Anda"</string>
|
||||
</resources>
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
<string name="banner_battery_optimization_title_android">"Kommer ikke varslene frem?"</string>
|
||||
<string name="banner_new_sound_message">"Varslingssignalet ditt er oppdatert – tydeligere, raskere og mindre forstyrrende."</string>
|
||||
<string name="banner_new_sound_title">"Vi har oppdatert lydene dine"</string>
|
||||
<string name="banner_set_up_recovery_content">"Gjenopprett din kryptografiske identitet og meldingshistorikk med en gjenopprettingsnøkkel hvis du har mistet alle dine brukte enheter."</string>
|
||||
<string name="banner_set_up_recovery_content">"Chattene dine sikkerhetskopieres automatisk med ende-til-ende-kryptering. For å gjenopprette denne sikkerhetskopien og beholde din digitale identitet når du mister tilgang til alle enhetene dine, trenger du gjenopprettingsnøkkelen din."</string>
|
||||
<string name="banner_set_up_recovery_submit">"Hent gjenopprettingsnøkkel"</string>
|
||||
<string name="banner_set_up_recovery_title">"Konfigurer gjenoppretting for å beskytte kontoen din"</string>
|
||||
<string name="banner_set_up_recovery_title">"Sikkerhetskopier chattene dine"</string>
|
||||
<string name="confirm_recovery_key_banner_message">"Verifiser gjenopprettingsnøkkelen for å opprettholde tilgangen til nøkkellageret og meldingshistorikken."</string>
|
||||
<string name="confirm_recovery_key_banner_primary_button_title">"Skriv inn gjenopprettingsnøkkelen din"</string>
|
||||
<string name="confirm_recovery_key_banner_secondary_button_title">"Har du glemt din gjenopprettingsnøkkel?"</string>
|
||||
|
||||
+1
-1
@@ -34,7 +34,7 @@ class RoomListContextMenuTest : RobolectricTest() {
|
||||
contextMenu = contextMenu,
|
||||
eventSink = eventsRecorder,
|
||||
)
|
||||
clickOn(R.string.screen_roomlist_mark_as_read)
|
||||
clickOn(CommonStrings.action_mark_as_read)
|
||||
eventsRecorder.assertList(
|
||||
listOf(
|
||||
RoomListEvent.HideContextMenu,
|
||||
|
||||
+2
-2
@@ -302,13 +302,13 @@ private fun InvitePeopleConfirmModal(
|
||||
text = stringResource(CommonStrings.action_remove),
|
||||
onClick = onRemove,
|
||||
leadingIcon = IconSource.Vector(CompoundIcons.Close()),
|
||||
modifier = Modifier.weight(1f).testTag(TestTags.confirmInviteUnknown),
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
Button(
|
||||
text = stringResource(CommonStrings.action_invite),
|
||||
onClick = onInvite,
|
||||
leadingIcon = IconSource.Vector(CompoundIcons.Check()),
|
||||
modifier = Modifier.weight(1f)
|
||||
modifier = Modifier.weight(1f).testTag(TestTags.confirmInviteUnknown),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,4 +2,8 @@
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_invite_users_already_a_member">"Sudah menjadi anggota"</string>
|
||||
<string name="screen_invite_users_already_invited">"Sudah diundang"</string>
|
||||
<string name="screen_invite_users_confirm_dialog_subtitle_multiple_users">"Saat ini Anda belum memiliki obrolan dengan kontak-kontak ini. Konfirmasikan undangan untuk mengundang mereka ke ruang obrolan ini sebelum melanjutkan."</string>
|
||||
<string name="screen_invite_users_confirm_dialog_subtitle_one_user">"Saat ini Anda belum memiliki obrolan apa pun dengan kontak ini. Konfirmasikan undangan ke ruang obrolan ini sebelum melanjutkan."</string>
|
||||
<string name="screen_invite_users_confirm_dialog_title_mutiple_users">"Ingin mengundang kontak baru ke ruang ini?"</string>
|
||||
<string name="screen_invite_users_confirm_dialog_title_one_user">"Ingin mengundang kontak baru ke ruang ini?"</string>
|
||||
</resources>
|
||||
|
||||
@@ -2,4 +2,8 @@
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_invite_users_already_a_member">"Allaqachon a\'zo"</string>
|
||||
<string name="screen_invite_users_already_invited">"Allaqachon taklif qilingan"</string>
|
||||
<string name="screen_invite_users_confirm_dialog_subtitle_multiple_users">"Hozirda ushbu kontaktlar bilan hech qanday chatingiz yoʻq. Davom etishdan oldin ularni ushbu xonaga taklif qilishni tasdiqlang."</string>
|
||||
<string name="screen_invite_users_confirm_dialog_subtitle_one_user">"Hozirda bu kontakt bilan hech qanday chatingiz yoʻq. Davom etishdan oldin ularni ushbu xonaga taklif qilishni tasdiqlang."</string>
|
||||
<string name="screen_invite_users_confirm_dialog_title_mutiple_users">"Bu xonaga yangi kontaktlar taklif qilinsinmi?"</string>
|
||||
<string name="screen_invite_users_confirm_dialog_title_one_user">"Bu xonaga yangi kontakt taklif qilinsinmi?"</string>
|
||||
</resources>
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
<string name="screen_link_new_device_mobile_step2">"گزینش %1$s"</string>
|
||||
<string name="screen_qr_code_login_connection_note_secure_state_description">"نتوانست اتّصالی امن به افزارهٔ جدید بسازد. افزارههای موجودتان هنوز امنند و نیازی نیست نگرانشان باشید."</string>
|
||||
<string name="screen_qr_code_login_connection_note_secure_state_list_header">"اکنون چه؟"</string>
|
||||
<string name="screen_qr_code_login_connection_note_secure_state_list_item_1">"آزمودن ورود دوباره با کد پاس در صورت مشکل شبکه"</string>
|
||||
<string name="screen_qr_code_login_connection_note_secure_state_list_item_2">"آزمون شبکهٔ وایفایی دیگر یا استفاده از دادهٔ همراهتان به جای وایفای در صورت مواجه دوباره با مشکل"</string>
|
||||
<string name="screen_qr_code_login_connection_note_secure_state_list_item_3">"ورود دستی در صورت کار نکردنش"</string>
|
||||
<string name="screen_qr_code_login_connection_note_secure_state_title">"اتّصال ناامن"</string>
|
||||
<string name="screen_qr_code_login_device_code_subtitle">"از شما خواسته خواهد شد که دو رقم نشان داده روی این افزاره را وارد کنید."</string>
|
||||
|
||||
@@ -1,16 +1,33 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_link_new_device_desktop_scanning_title">"Pindai kode QR"</string>
|
||||
<string name="screen_link_new_device_desktop_step1">"Buka %1$s di laptop atau komputer desktop."</string>
|
||||
<string name="screen_link_new_device_desktop_step3">"Pindai kode QR dengan perangkat ini"</string>
|
||||
<string name="screen_link_new_device_desktop_submit">"Siap untuk memindai"</string>
|
||||
<string name="screen_link_new_device_desktop_title">"Buka %1$s di komputer desktop untuk mendapatkan kode QR."</string>
|
||||
<string name="screen_link_new_device_enter_number_error_numbers_do_not_match">"Angkanya tidak cocok"</string>
|
||||
<string name="screen_link_new_device_enter_number_notice">"Masukkan kode 2 digit"</string>
|
||||
<string name="screen_link_new_device_enter_number_subtitle">"Ini akan memverifikasi bahwa koneksi ke perangkat Anda yang lain aman."</string>
|
||||
<string name="screen_link_new_device_enter_number_title">"Masukkan nomor yang ditampilkan di perangkat Anda yang lain"</string>
|
||||
<string name="screen_link_new_device_error_app_not_supported_subtitle">"Penyedia akun Anda tidak mendukung %1$s."</string>
|
||||
<string name="screen_link_new_device_error_app_not_supported_title">"%1$s tidak didukung"</string>
|
||||
<string name="screen_link_new_device_error_not_supported_subtitle">"Penyedia akun Anda tidak mendukung masuk ke perangkat baru menggunakan kode QR."</string>
|
||||
<string name="screen_link_new_device_error_not_supported_title">"Kode QR tidak didukung"</string>
|
||||
<string name="screen_link_new_device_error_request_cancelled_subtitle">"Proses masuk dibatalkan di perangkat lain."</string>
|
||||
<string name="screen_link_new_device_error_request_cancelled_title">"Permintaan masuk dibatalkan"</string>
|
||||
<string name="screen_link_new_device_error_request_timeout_subtitle">"Masa masuk kedaluwarsa. Silakan coba lagi."</string>
|
||||
<string name="screen_link_new_device_error_request_timeout_title">"Proses masuk tidak selesai tepat waktu"</string>
|
||||
<string name="screen_link_new_device_mobile_step1">"Buka %1$s di perangkat lain."</string>
|
||||
<string name="screen_link_new_device_mobile_step2">"Pilih %1$s"</string>
|
||||
<string name="screen_link_new_device_mobile_step2_action">"Masuk dengan kode QR"</string>
|
||||
<string name="screen_link_new_device_mobile_step3">"Pindai kode QR yang ditampilkan di sini dengan perangkat lain."</string>
|
||||
<string name="screen_link_new_device_mobile_title">"Buka %1$s di perangkat lain."</string>
|
||||
<string name="screen_link_new_device_root_desktop_computer">"Komputer desktop"</string>
|
||||
<string name="screen_link_new_device_root_loading_qr_code">"Memuat kode QR…"</string>
|
||||
<string name="screen_link_new_device_root_mobile_device">"Perangkat seluler"</string>
|
||||
<string name="screen_link_new_device_root_title">"Jenis perangkat apa yang ingin Anda tautkan?"</string>
|
||||
<string name="screen_link_new_device_wrong_number_subtitle">"Silakan coba lagi dan pastikan Anda telah memasukkan kode 2 digit dengan benar. Jika angka-angka tersebut masih tidak cocok, hubungi penyedia akun Anda."</string>
|
||||
<string name="screen_link_new_device_wrong_number_title">"Angkanya tidak cocok"</string>
|
||||
<string name="screen_qr_code_login_connection_note_secure_state_description">"Koneksi aman tidak dapat dibuat ke perangkat baru. Perangkat Anda yang ada masih aman dan Anda tidak perlu khawatir tentang mereka."</string>
|
||||
<string name="screen_qr_code_login_connection_note_secure_state_list_header">"Apa sekarang?"</string>
|
||||
<string name="screen_qr_code_login_connection_note_secure_state_list_item_1">"Coba masuk lagi dengan kode QR jika ini adalah masalah jaringan"</string>
|
||||
@@ -23,6 +40,8 @@
|
||||
<string name="screen_qr_code_login_error_cancelled_title">"Permintaan masuk dibatalkan"</string>
|
||||
<string name="screen_qr_code_login_error_declined_subtitle">"Proses masuk ditolak di perangkat lain."</string>
|
||||
<string name="screen_qr_code_login_error_declined_title">"Proses masuk ditolak"</string>
|
||||
<string name="screen_qr_code_login_error_device_already_signed_in_subtitle">"Anda tidak perlu melakukan hal lain."</string>
|
||||
<string name="screen_qr_code_login_error_device_already_signed_in_title">"Perangkat Anda yang lain sudah masuk"</string>
|
||||
<string name="screen_qr_code_login_error_expired_subtitle">"Masa masuk kedaluwarsa. Silakan coba lagi."</string>
|
||||
<string name="screen_qr_code_login_error_expired_title">"Proses masuk tidak selesai tepat waktu"</string>
|
||||
<string name="screen_qr_code_login_error_linking_not_suported_subtitle">"Perangkat Anda yang lain tidak mendukung masuk ke %s dengan kode QR.
|
||||
|
||||
+4
-2
@@ -30,7 +30,7 @@ import androidx.compose.material3.SheetValue
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.rememberBottomSheetScaffoldState
|
||||
import androidx.compose.material3.rememberStandardBottomSheetState
|
||||
import androidx.compose.material3.rememberBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
@@ -86,7 +86,9 @@ fun MapBottomSheetScaffold(
|
||||
customMapStyleUrl: AsyncData<String?>,
|
||||
modifier: Modifier = Modifier,
|
||||
scaffoldState: BottomSheetScaffoldState = rememberBottomSheetScaffoldState(
|
||||
bottomSheetState = rememberStandardBottomSheetState(initialValue = SheetValue.PartiallyExpanded)
|
||||
bottomSheetState = rememberBottomSheetState(
|
||||
initialValue = SheetValue.PartiallyExpanded,
|
||||
)
|
||||
),
|
||||
cameraState: CameraState = rememberCameraState(),
|
||||
mapOptions: MapOptions = MapDefaults.options,
|
||||
|
||||
+4
-2
@@ -20,7 +20,7 @@ import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.SheetValue
|
||||
import androidx.compose.material3.rememberBottomSheetScaffoldState
|
||||
import androidx.compose.material3.rememberStandardBottomSheetState
|
||||
import androidx.compose.material3.rememberBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
@@ -105,7 +105,9 @@ fun ShareLocationView(
|
||||
}
|
||||
|
||||
val scaffoldState = rememberBottomSheetScaffoldState(
|
||||
bottomSheetState = rememberStandardBottomSheetState(initialValue = SheetValue.Expanded)
|
||||
bottomSheetState = rememberBottomSheetState(
|
||||
initialValue = SheetValue.Expanded,
|
||||
)
|
||||
)
|
||||
val cameraState = rememberCameraState(firstPosition = MapDefaults.defaultCameraPosition)
|
||||
|
||||
|
||||
+4
-2
@@ -21,7 +21,7 @@ import androidx.compose.material3.BottomSheetDefaults
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.SheetValue
|
||||
import androidx.compose.material3.rememberBottomSheetScaffoldState
|
||||
import androidx.compose.material3.rememberStandardBottomSheetState
|
||||
import androidx.compose.material3.rememberBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
@@ -92,7 +92,9 @@ fun ShowLocationView(
|
||||
}
|
||||
|
||||
val scaffoldState = rememberBottomSheetScaffoldState(
|
||||
bottomSheetState = rememberStandardBottomSheetState(SheetValue.Expanded)
|
||||
bottomSheetState = rememberBottomSheetState(
|
||||
initialValue = SheetValue.Expanded,
|
||||
)
|
||||
)
|
||||
LaunchedEffect(state.isSheetDraggable) {
|
||||
if (!state.isSheetDraggable) {
|
||||
|
||||
@@ -2,4 +2,5 @@
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_share_location_live_location_disclaimer_title">"Vaša povijest lokacije uživo bit će pohranjena u sobi i vidljiva članovima nakon završetka sesije."</string>
|
||||
<string name="screen_share_location_live_location_duration_picker_title">"Odaberite koliko dugo želite dijeliti svoju lokaciju uživo."</string>
|
||||
<string name="screen_share_location_live_location_missing_permissions">"Nemate dopuštenja za dijeljenje svoje lokacije uživo u ovoj sobi"</string>
|
||||
</resources>
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_share_location_live_location_disclaimer_title">"Riwayat lokasi langsung Anda akan disimpan di ruang obrolan dan dapat dilihat oleh anggota setelah sesi berakhir."</string>
|
||||
<string name="screen_share_location_live_location_duration_picker_title">"Pilih berapa lama Anda ingin membagikan lokasi langsung Anda."</string>
|
||||
<string name="screen_share_location_live_location_missing_permissions">"Anda tidak memiliki izin untuk membagikan lokasi real-time Anda di ruang ini"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_share_location_live_location_duration_picker_title">"Velg hvor lenge du vil dele din sanntidslokasjon."</string>
|
||||
</resources>
|
||||
@@ -2,4 +2,5 @@
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_share_location_live_location_disclaimer_title">"Jonli joylashuv tarixingiz chat-xonada saqlanadi va sessiya tugaganidan keyin homiylarga ko‘rinadi."</string>
|
||||
<string name="screen_share_location_live_location_duration_picker_title">"Jonli joylashuvingiz qancha vaqt ulashilishini tanlang."</string>
|
||||
<string name="screen_share_location_live_location_missing_permissions">"Bu xonada jonli joylashuvingizni ulashish uchun sizda ruxsat yoʻq"</string>
|
||||
</resources>
|
||||
|
||||
+49
-18
@@ -15,15 +15,23 @@ import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.res.pluralStringResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.clearAndSetSemantics
|
||||
import androidx.compose.ui.semantics.contentDescription
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
@@ -32,6 +40,8 @@ import io.element.android.features.lockscreen.impl.pin.model.PinEntry
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.theme.pinDigitBg
|
||||
import io.element.android.libraries.ui.strings.CommonPlurals
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
|
||||
@Composable
|
||||
fun PinEntryTextField(
|
||||
@@ -39,16 +49,25 @@ fun PinEntryTextField(
|
||||
isSecured: Boolean,
|
||||
onValueChange: (String) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
initialIsFocus: Boolean = false,
|
||||
) {
|
||||
var isFocused by remember { mutableStateOf(initialIsFocus) }
|
||||
val filledCount = pinEntry.digits.count { it is PinDigit.Filled }
|
||||
val pinFieldLabel = stringResource(CommonStrings.a11y_pin_field)
|
||||
val digitsEnteredLabel = pluralStringResource(CommonPlurals.a11y_digits_entered, filledCount, filledCount)
|
||||
BasicTextField(
|
||||
modifier = modifier,
|
||||
modifier = modifier
|
||||
.onFocusChanged { isFocused = it.isFocused }
|
||||
.clearAndSetSemantics {
|
||||
contentDescription = "$pinFieldLabel, $digitsEnteredLabel"
|
||||
},
|
||||
value = pinEntry.toText(),
|
||||
onValueChange = {
|
||||
onValueChange(it)
|
||||
},
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.NumberPassword),
|
||||
decorationBox = {
|
||||
PinEntryRow(pinEntry = pinEntry, isSecured = isSecured)
|
||||
PinEntryRow(pinEntry = pinEntry, isSecured = isSecured, isFocused = isFocused)
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -58,13 +77,19 @@ fun PinEntryTextField(
|
||||
private fun PinEntryRow(
|
||||
pinEntry: PinEntry,
|
||||
isSecured: Boolean,
|
||||
isFocused: Boolean,
|
||||
) {
|
||||
FlowRow(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp, alignment = Alignment.CenterHorizontally),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
for (digit in pinEntry.digits) {
|
||||
PinDigitView(digit = digit, isSecured = isSecured)
|
||||
val focusedIndex = pinEntry.digits.indexOfFirst { it is PinDigit.Empty }
|
||||
pinEntry.digits.forEachIndexed { index, digit ->
|
||||
PinDigitView(
|
||||
digit = digit,
|
||||
isSecured = isSecured,
|
||||
isFocused = isFocused && index == focusedIndex
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -73,11 +98,16 @@ private fun PinEntryRow(
|
||||
private fun PinDigitView(
|
||||
digit: PinDigit,
|
||||
isSecured: Boolean,
|
||||
isFocused: Boolean,
|
||||
) {
|
||||
val shape = RoundedCornerShape(8.dp)
|
||||
val appearanceModifier = when (digit) {
|
||||
PinDigit.Empty -> {
|
||||
Modifier.border(1.dp, ElementTheme.colors.iconPrimary, shape)
|
||||
if (isFocused) {
|
||||
Modifier.border(2.dp, ElementTheme.colors.borderFocused, shape)
|
||||
} else {
|
||||
Modifier.border(1.dp, ElementTheme.colors.iconPrimary, shape)
|
||||
}
|
||||
}
|
||||
is PinDigit.Filled -> {
|
||||
Modifier.background(ElementTheme.colors.pinDigitBg, shape)
|
||||
@@ -108,18 +138,19 @@ private fun PinDigitView(
|
||||
internal fun PinEntryTextFieldPreview() {
|
||||
ElementPreview {
|
||||
val pinEntry = PinEntry.createEmpty(4).fillWith("12")
|
||||
Column {
|
||||
PinEntryTextField(
|
||||
pinEntry = pinEntry,
|
||||
isSecured = true,
|
||||
onValueChange = {},
|
||||
)
|
||||
Spacer(modifier = Modifier.size(16.dp))
|
||||
PinEntryTextField(
|
||||
pinEntry = pinEntry,
|
||||
isSecured = false,
|
||||
onValueChange = {},
|
||||
)
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
listOf(true, false).forEach { isSecured ->
|
||||
listOf(true, false).forEach { initialIsFocus ->
|
||||
PinEntryTextField(
|
||||
pinEntry = pinEntry,
|
||||
isSecured = isSecured,
|
||||
initialIsFocus = initialIsFocus,
|
||||
onValueChange = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
@@ -13,4 +13,6 @@ import io.element.android.features.login.impl.accountprovider.AccountProvider
|
||||
sealed interface ChangeServerEvents {
|
||||
data class ChangeServer(val accountProvider: AccountProvider) : ChangeServerEvents
|
||||
data object ClearError : ChangeServerEvents
|
||||
data object RequestLocalNetworkPermission : ChangeServerEvents
|
||||
data object DismissLocalNetworkPermission : ChangeServerEvents
|
||||
}
|
||||
|
||||
+13
-8
@@ -12,50 +12,55 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import dev.zacsweers.metro.Inject
|
||||
import io.element.android.features.login.impl.accesscontrol.DefaultAccountProviderAccessControl
|
||||
import io.element.android.features.login.impl.accountprovider.AccountProvider
|
||||
import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
|
||||
import io.element.android.features.login.impl.error.ChangeServerError
|
||||
import io.element.android.features.login.impl.localnetwork.LocalNetworkPermissionGate
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.architecture.runCatchingUpdatingState
|
||||
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Inject
|
||||
class ChangeServerPresenter(
|
||||
private val authenticationService: MatrixAuthenticationService,
|
||||
private val accountProviderDataSource: AccountProviderDataSource,
|
||||
private val defaultAccountProviderAccessControl: DefaultAccountProviderAccessControl,
|
||||
private val localNetworkPermissionGate: LocalNetworkPermissionGate,
|
||||
) : Presenter<ChangeServerState> {
|
||||
@Composable
|
||||
override fun present(): ChangeServerState {
|
||||
val localCoroutineScope = rememberCoroutineScope()
|
||||
|
||||
val changeServerAction: MutableState<AsyncData<Unit>> = remember {
|
||||
mutableStateOf(AsyncData.Uninitialized)
|
||||
}
|
||||
|
||||
val gateState = localNetworkPermissionGate.present<AccountProvider>(
|
||||
urlOf = { provider -> provider.url },
|
||||
onProceed = { provider -> changeServer(provider, changeServerAction) },
|
||||
)
|
||||
|
||||
fun handleEvent(event: ChangeServerEvents) {
|
||||
when (event) {
|
||||
is ChangeServerEvents.ChangeServer -> localCoroutineScope.changeServer(event.accountProvider, changeServerAction)
|
||||
ChangeServerEvents.ClearError -> changeServerAction.value = AsyncData.Uninitialized
|
||||
is ChangeServerEvents.ChangeServer -> gateState.submit(event.accountProvider)
|
||||
ChangeServerEvents.DismissLocalNetworkPermission -> gateState.abort()
|
||||
ChangeServerEvents.RequestLocalNetworkPermission -> gateState.requestPermission()
|
||||
}
|
||||
}
|
||||
|
||||
return ChangeServerState(
|
||||
changeServerAction = changeServerAction.value,
|
||||
localNetworkPermissionDialog = gateState.dialog,
|
||||
eventSink = ::handleEvent,
|
||||
)
|
||||
}
|
||||
|
||||
private fun CoroutineScope.changeServer(
|
||||
private suspend fun changeServer(
|
||||
data: AccountProvider,
|
||||
changeServerAction: MutableState<AsyncData<Unit>>,
|
||||
) = launch {
|
||||
) {
|
||||
suspend {
|
||||
defaultAccountProviderAccessControl.assertIsAllowedToConnectToAccountProvider(
|
||||
title = data.title,
|
||||
|
||||
+2
@@ -9,8 +9,10 @@
|
||||
package io.element.android.features.login.impl.changeserver
|
||||
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionDialog
|
||||
|
||||
data class ChangeServerState(
|
||||
val changeServerAction: AsyncData<Unit>,
|
||||
val localNetworkPermissionDialog: LocalNetworkPermissionDialog,
|
||||
val eventSink: (ChangeServerEvents) -> Unit
|
||||
)
|
||||
|
||||
+4
@@ -11,6 +11,7 @@ package io.element.android.features.login.impl.changeserver
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.features.login.impl.error.ChangeServerError
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionDialog
|
||||
|
||||
open class ChangeServerStateProvider : PreviewParameterProvider<ChangeServerState> {
|
||||
override val values: Sequence<ChangeServerState>
|
||||
@@ -39,12 +40,15 @@ open class ChangeServerStateProvider : PreviewParameterProvider<ChangeServerStat
|
||||
ChangeServerError.UnsupportedServer
|
||||
)
|
||||
),
|
||||
aChangeServerState(localNetworkPermissionDialog = LocalNetworkPermissionDialog.Rationale),
|
||||
)
|
||||
}
|
||||
|
||||
fun aChangeServerState(
|
||||
changeServerAction: AsyncData<Unit> = AsyncData.Uninitialized,
|
||||
localNetworkPermissionDialog: LocalNetworkPermissionDialog = LocalNetworkPermissionDialog.None,
|
||||
) = ChangeServerState(
|
||||
changeServerAction = changeServerAction,
|
||||
localNetworkPermissionDialog = localNetworkPermissionDialog,
|
||||
eventSink = {}
|
||||
)
|
||||
|
||||
+10
@@ -27,6 +27,7 @@ import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.theme.LocalBuildMeta
|
||||
import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionDialogView
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
|
||||
@Composable
|
||||
@@ -121,6 +122,15 @@ fun ChangeServerView(
|
||||
}
|
||||
AsyncData.Uninitialized -> Unit
|
||||
}
|
||||
LocalNetworkPermissionDialogView(
|
||||
dialog = state.localNetworkPermissionDialog,
|
||||
onSubmit = {
|
||||
eventSink.invoke(ChangeServerEvents.RequestLocalNetworkPermission)
|
||||
},
|
||||
onDismiss = {
|
||||
eventSink.invoke(ChangeServerEvents.DismissLocalNetworkPermission)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
|
||||
+5
@@ -14,6 +14,8 @@ import dev.zacsweers.metro.Binds
|
||||
import dev.zacsweers.metro.ContributesTo
|
||||
import io.element.android.features.login.impl.changeserver.ChangeServerPresenter
|
||||
import io.element.android.features.login.impl.changeserver.ChangeServerState
|
||||
import io.element.android.features.login.impl.login.LoginModePresenter
|
||||
import io.element.android.features.login.impl.login.LoginModeState
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
|
||||
@ContributesTo(AppScope::class)
|
||||
@@ -21,4 +23,7 @@ import io.element.android.libraries.architecture.Presenter
|
||||
interface LoginModule {
|
||||
@Binds
|
||||
fun bindChangeServerPresenter(presenter: ChangeServerPresenter): Presenter<ChangeServerState>
|
||||
|
||||
@Binds
|
||||
fun bindLoginModePresenter(presenter: LoginModePresenter): Presenter<LoginModeState>
|
||||
}
|
||||
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.login.impl.localnetwork
|
||||
|
||||
import android.Manifest
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.runtime.setValue
|
||||
import dev.zacsweers.metro.Inject
|
||||
import io.element.android.libraries.permissions.api.PermissionsEvent
|
||||
import io.element.android.libraries.permissions.api.PermissionsPresenter
|
||||
import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionAdvisor
|
||||
import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionDialog
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Inject
|
||||
class LocalNetworkPermissionGate(
|
||||
private val advisor: LocalNetworkPermissionAdvisor,
|
||||
permissionsPresenterFactory: PermissionsPresenter.Factory,
|
||||
) {
|
||||
private val permissionsPresenter: PermissionsPresenter =
|
||||
permissionsPresenterFactory.create(Manifest.permission.ACCESS_LOCAL_NETWORK)
|
||||
|
||||
@Composable
|
||||
fun <T : Any> present(
|
||||
urlOf: (T) -> String,
|
||||
onProceed: suspend (T) -> Unit,
|
||||
): LocalNetworkPermissionGateState<T> {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val permissionsState = permissionsPresenter.present()
|
||||
var pendingSubmit by remember { mutableStateOf<T?>(null) }
|
||||
|
||||
val latestUrlOf by rememberUpdatedState(urlOf)
|
||||
val latestOnProceed by rememberUpdatedState(onProceed)
|
||||
|
||||
LaunchedEffect(permissionsState.permissionGranted, pendingSubmit) {
|
||||
val pending = pendingSubmit
|
||||
if (pending != null && permissionsState.permissionGranted) {
|
||||
coroutineScope.launch { latestOnProceed(pending) }
|
||||
pendingSubmit = null
|
||||
}
|
||||
}
|
||||
|
||||
val dialog by rememberUpdatedState(
|
||||
when {
|
||||
pendingSubmit == null -> LocalNetworkPermissionDialog.None
|
||||
permissionsState.permissionGranted -> LocalNetworkPermissionDialog.None
|
||||
permissionsState.shouldShowRationale || !permissionsState.permissionAlreadyAsked -> LocalNetworkPermissionDialog.Rationale
|
||||
else -> LocalNetworkPermissionDialog.Settings
|
||||
}
|
||||
)
|
||||
|
||||
fun submit(value: T) {
|
||||
coroutineScope.launch {
|
||||
if (advisor.shouldRequestPermissionFor(latestUrlOf(value))) {
|
||||
pendingSubmit = value
|
||||
} else {
|
||||
latestOnProceed(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun requestPermission() {
|
||||
when (dialog) {
|
||||
LocalNetworkPermissionDialog.Settings -> {
|
||||
permissionsState.eventSink(PermissionsEvent.OpenSystemSettingAndCloseDialog)
|
||||
}
|
||||
LocalNetworkPermissionDialog.Rationale -> {
|
||||
permissionsState.eventSink(PermissionsEvent.ForceRequestPermissions)
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
fun abort() {
|
||||
pendingSubmit = null
|
||||
}
|
||||
|
||||
return LocalNetworkPermissionGateState(
|
||||
dialog = dialog,
|
||||
submit = ::submit,
|
||||
requestPermission = ::requestPermission,
|
||||
abort = ::abort,
|
||||
)
|
||||
}
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.login.impl.localnetwork
|
||||
|
||||
import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionDialog
|
||||
|
||||
data class LocalNetworkPermissionGateState<T>(
|
||||
val dialog: LocalNetworkPermissionDialog,
|
||||
val submit: (T) -> Unit,
|
||||
val requestPermission: () -> Unit,
|
||||
val abort: () -> Unit,
|
||||
)
|
||||
-128
@@ -1,128 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.login.impl.login
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import dev.zacsweers.metro.Inject
|
||||
import io.element.android.features.login.impl.error.ChangeServerError
|
||||
import io.element.android.features.login.impl.screens.chooseaccountprovider.ChooseAccountProviderPresenter
|
||||
import io.element.android.features.login.impl.screens.confirmaccountprovider.ConfirmAccountProviderPresenter
|
||||
import io.element.android.features.login.impl.screens.createaccount.AccountCreationNotSupported
|
||||
import io.element.android.features.login.impl.screens.onboarding.OnBoardingPresenter
|
||||
import io.element.android.features.login.impl.web.WebClientUrlForAuthenticationRetriever
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.architecture.runCatchingUpdatingState
|
||||
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
|
||||
import io.element.android.libraries.matrix.api.auth.OAuthPrompt
|
||||
import io.element.android.libraries.oauth.api.OAuthAction
|
||||
import io.element.android.libraries.oauth.api.OAuthActionFlow
|
||||
|
||||
/**
|
||||
* This class is responsible for managing the login flow, including handling OIDC actions and
|
||||
* submitting login requests.
|
||||
* It's a helper to avoid code duplication. It is used by [OnBoardingPresenter], [ConfirmAccountProviderPresenter]
|
||||
* and [ChooseAccountProviderPresenter].
|
||||
*/
|
||||
@Inject
|
||||
class LoginHelper(
|
||||
private val oAuthActionFlow: OAuthActionFlow,
|
||||
private val authenticationService: MatrixAuthenticationService,
|
||||
private val webClientUrlForAuthenticationRetriever: WebClientUrlForAuthenticationRetriever,
|
||||
) {
|
||||
private val loginModeState: MutableState<AsyncData<LoginMode>> = mutableStateOf(AsyncData.Uninitialized)
|
||||
|
||||
@Composable
|
||||
fun collectLoginMode(): State<AsyncData<LoginMode>> {
|
||||
LaunchedEffect(Unit) {
|
||||
oAuthActionFlow.collect { oAuthAction ->
|
||||
if (oAuthAction != null) {
|
||||
onOAuthAction(oAuthAction)
|
||||
}
|
||||
}
|
||||
}
|
||||
return loginModeState
|
||||
}
|
||||
|
||||
fun clearError() {
|
||||
loginModeState.value = AsyncData.Uninitialized
|
||||
}
|
||||
|
||||
suspend fun submit(
|
||||
isAccountCreation: Boolean,
|
||||
homeserverUrl: String,
|
||||
resolvedHomeserverUrl: String?,
|
||||
loginHint: String?,
|
||||
) {
|
||||
suspend {
|
||||
authenticationService.setHomeserver(homeserverUrl).recoverCatching {
|
||||
// No .well-known file?
|
||||
// If the homeserver is not reachable, try using resolvedHomeserverUrl.
|
||||
if (resolvedHomeserverUrl != null && resolvedHomeserverUrl != homeserverUrl) {
|
||||
authenticationService.setHomeserver(resolvedHomeserverUrl).getOrThrow()
|
||||
} else {
|
||||
throw it
|
||||
}
|
||||
}.map { matrixHomeServerDetails ->
|
||||
if (matrixHomeServerDetails.supportsOAuthLogin) {
|
||||
// Retrieve the details right now
|
||||
val oAuthPrompt = if (isAccountCreation) OAuthPrompt.Create else OAuthPrompt.Login
|
||||
LoginMode.OAuth(
|
||||
authenticationService.getOAuthUrl(prompt = oAuthPrompt, loginHint = loginHint).getOrThrow()
|
||||
)
|
||||
} else if (isAccountCreation) {
|
||||
val url = webClientUrlForAuthenticationRetriever.retrieve(homeserverUrl)
|
||||
LoginMode.AccountCreation(url)
|
||||
} else if (matrixHomeServerDetails.supportsPasswordLogin) {
|
||||
LoginMode.PasswordLogin
|
||||
} else {
|
||||
error("Unsupported login flow")
|
||||
}
|
||||
}.getOrThrow()
|
||||
}.runCatchingUpdatingState(
|
||||
state = loginModeState,
|
||||
errorTransform = {
|
||||
when (it) {
|
||||
is AccountCreationNotSupported -> it
|
||||
else -> ChangeServerError.from(it)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun onOAuthAction(oAuthAction: OAuthAction) {
|
||||
if (oAuthAction is OAuthAction.GoBack && oAuthAction.toUnblock && loginModeState.value !is AsyncData.Loading) {
|
||||
// Ignore GoBack action if the current state is not Loading. This GoBack action is coming from LoginFlowNode.
|
||||
// This can happen if there is an error, for instance attempt to login again on the same account.
|
||||
return
|
||||
}
|
||||
loginModeState.value = AsyncData.Loading()
|
||||
when (oAuthAction) {
|
||||
is OAuthAction.GoBack -> {
|
||||
authenticationService.cancelOAuthLogin()
|
||||
.onSuccess {
|
||||
loginModeState.value = AsyncData.Uninitialized
|
||||
}
|
||||
.onFailure { failure ->
|
||||
loginModeState.value = AsyncData.Failure(failure)
|
||||
}
|
||||
}
|
||||
is OAuthAction.Success -> {
|
||||
authenticationService.loginWithOAuth(oAuthAction.url)
|
||||
.onFailure { failure ->
|
||||
loginModeState.value = AsyncData.Failure(failure)
|
||||
}
|
||||
}
|
||||
}
|
||||
oAuthActionFlow.reset()
|
||||
}
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.login.impl.login
|
||||
|
||||
sealed interface LoginModeEvent {
|
||||
data class Submit(
|
||||
val isAccountCreation: Boolean,
|
||||
val homeserverUrl: String,
|
||||
val resolvedHomeserverUrl: String?,
|
||||
val loginHint: String?,
|
||||
) : LoginModeEvent
|
||||
|
||||
data object ClearError : LoginModeEvent
|
||||
|
||||
data object RequestLocalNetworkPermission : LoginModeEvent
|
||||
|
||||
data object DismissLocalNetworkPermission : LoginModeEvent
|
||||
}
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.login.impl.login
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import dev.zacsweers.metro.Inject
|
||||
import io.element.android.features.login.impl.error.ChangeServerError
|
||||
import io.element.android.features.login.impl.localnetwork.LocalNetworkPermissionGate
|
||||
import io.element.android.features.login.impl.screens.chooseaccountprovider.ChooseAccountProviderPresenter
|
||||
import io.element.android.features.login.impl.screens.classic.loginwithclassic.LoginWithClassicPresenter
|
||||
import io.element.android.features.login.impl.screens.confirmaccountprovider.ConfirmAccountProviderPresenter
|
||||
import io.element.android.features.login.impl.screens.createaccount.AccountCreationNotSupported
|
||||
import io.element.android.features.login.impl.screens.onboarding.OnBoardingPresenter
|
||||
import io.element.android.features.login.impl.web.WebClientUrlForAuthenticationRetriever
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.architecture.runCatchingUpdatingState
|
||||
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
|
||||
import io.element.android.libraries.matrix.api.auth.OAuthPrompt
|
||||
import io.element.android.libraries.oauth.api.OAuthAction
|
||||
import io.element.android.libraries.oauth.api.OAuthActionFlow
|
||||
|
||||
/**
|
||||
* Presenter responsible for managing the login flow, including handling OAuth actions and
|
||||
* submitting login requests.
|
||||
* It's a helper to avoid code duplication. It is used by [OnBoardingPresenter], [ConfirmAccountProviderPresenter],
|
||||
* [ChooseAccountProviderPresenter] and [LoginWithClassicPresenter].
|
||||
*/
|
||||
@Inject
|
||||
class LoginModePresenter(
|
||||
private val oAuthActionFlow: OAuthActionFlow,
|
||||
private val authenticationService: MatrixAuthenticationService,
|
||||
private val webClientUrlForAuthenticationRetriever: WebClientUrlForAuthenticationRetriever,
|
||||
private val localNetworkPermissionGate: LocalNetworkPermissionGate,
|
||||
) : Presenter<LoginModeState> {
|
||||
@Composable
|
||||
override fun present(): LoginModeState {
|
||||
val loginMode: MutableState<AsyncData<LoginMode>> = remember { mutableStateOf(AsyncData.Uninitialized) }
|
||||
|
||||
val gateState = localNetworkPermissionGate.present<LoginModeEvent.Submit>(
|
||||
urlOf = { it.homeserverUrl },
|
||||
onProceed = { request -> performSubmit(request, loginMode) },
|
||||
)
|
||||
|
||||
// Forward OAuth navigation events to the presenter's own loginMode state.
|
||||
LaunchedEffect(Unit) {
|
||||
oAuthActionFlow.collect { action ->
|
||||
if (action != null) handleOAuthAction(action, loginMode)
|
||||
}
|
||||
}
|
||||
|
||||
fun handleEvent(event: LoginModeEvent) {
|
||||
when (event) {
|
||||
LoginModeEvent.ClearError -> loginMode.value = AsyncData.Uninitialized
|
||||
is LoginModeEvent.Submit -> gateState.submit(event)
|
||||
LoginModeEvent.DismissLocalNetworkPermission -> gateState.abort()
|
||||
LoginModeEvent.RequestLocalNetworkPermission -> gateState.requestPermission()
|
||||
}
|
||||
}
|
||||
return LoginModeState(
|
||||
loginMode = loginMode.value,
|
||||
localNetworkPermissionDialog = gateState.dialog,
|
||||
eventSink = ::handleEvent,
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun performSubmit(request: LoginModeEvent.Submit, loginMode: MutableState<AsyncData<LoginMode>>) {
|
||||
suspend {
|
||||
authenticationService.setHomeserver(request.homeserverUrl).recoverCatching {
|
||||
// Fallback to the well-known-resolved URL if the primary URL failed and the caller supplied one.
|
||||
if (request.resolvedHomeserverUrl != null && request.resolvedHomeserverUrl != request.homeserverUrl) {
|
||||
authenticationService.setHomeserver(request.resolvedHomeserverUrl).getOrThrow()
|
||||
} else {
|
||||
throw it
|
||||
}
|
||||
}.map { matrixHomeServerDetails ->
|
||||
when {
|
||||
matrixHomeServerDetails.supportsOAuthLogin -> {
|
||||
val oAuthPrompt = if (request.isAccountCreation) OAuthPrompt.Create else OAuthPrompt.Login
|
||||
LoginMode.OAuth(
|
||||
authenticationService.getOAuthUrl(prompt = oAuthPrompt, loginHint = request.loginHint).getOrThrow()
|
||||
)
|
||||
}
|
||||
request.isAccountCreation -> LoginMode.AccountCreation(
|
||||
webClientUrlForAuthenticationRetriever.retrieve(request.homeserverUrl)
|
||||
)
|
||||
matrixHomeServerDetails.supportsPasswordLogin -> LoginMode.PasswordLogin
|
||||
else -> error("Unsupported login flow")
|
||||
}
|
||||
}.getOrThrow()
|
||||
}.runCatchingUpdatingState(
|
||||
state = loginMode,
|
||||
errorTransform = {
|
||||
when (it) {
|
||||
is AccountCreationNotSupported -> it
|
||||
else -> ChangeServerError.from(it)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun handleOAuthAction(action: OAuthAction, loginMode: MutableState<AsyncData<LoginMode>>) {
|
||||
if (action is OAuthAction.GoBack && action.toUnblock && loginMode.value !is AsyncData.Loading) {
|
||||
// Ignore GoBack that isn't tied to an in-flight login — it comes from LoginFlowNode
|
||||
// when the user backs out after a completed login attempt.
|
||||
return
|
||||
}
|
||||
loginMode.value = AsyncData.Loading()
|
||||
when (action) {
|
||||
is OAuthAction.GoBack -> authenticationService.cancelOAuthLogin()
|
||||
.onSuccess { loginMode.value = AsyncData.Uninitialized }
|
||||
.onFailure { loginMode.value = AsyncData.Failure(it) }
|
||||
is OAuthAction.Success -> authenticationService.loginWithOAuth(action.url)
|
||||
.onFailure { loginMode.value = AsyncData.Failure(it) }
|
||||
}
|
||||
oAuthActionFlow.reset()
|
||||
}
|
||||
}
|
||||
+9
-2
@@ -1,6 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
* Copyright (c) 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
@@ -8,7 +7,15 @@
|
||||
|
||||
package io.element.android.features.login.impl.login
|
||||
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.matrix.api.auth.OAuthDetails
|
||||
import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionDialog
|
||||
|
||||
data class LoginModeState(
|
||||
val loginMode: AsyncData<LoginMode>,
|
||||
val localNetworkPermissionDialog: LocalNetworkPermissionDialog,
|
||||
val eventSink: (LoginModeEvent) -> Unit,
|
||||
)
|
||||
|
||||
sealed interface LoginMode {
|
||||
data object PasswordLogin : LoginMode
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.login.impl.login
|
||||
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionDialog
|
||||
|
||||
fun aLoginModeState(
|
||||
loginMode: AsyncData<LoginMode> = AsyncData.Uninitialized,
|
||||
localNetworkPermissionDialog: LocalNetworkPermissionDialog = LocalNetworkPermissionDialog.None,
|
||||
eventSink: (LoginModeEvent) -> Unit = {},
|
||||
) = LoginModeState(
|
||||
loginMode = loginMode,
|
||||
localNetworkPermissionDialog = localNetworkPermissionDialog,
|
||||
eventSink = eventSink,
|
||||
)
|
||||
+11
-5
@@ -14,6 +14,7 @@ import io.element.android.libraries.core.coroutine.parallelMap
|
||||
import io.element.android.libraries.core.uri.ensureProtocol
|
||||
import io.element.android.libraries.core.uri.isValidUrl
|
||||
import io.element.android.libraries.matrix.api.auth.HomeServerLoginCompatibilityChecker
|
||||
import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionAdvisor
|
||||
import kotlinx.coroutines.currentCoroutineContext
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
@@ -28,6 +29,7 @@ import java.util.Collections
|
||||
class HomeserverResolver(
|
||||
private val dispatchers: CoroutineDispatchers,
|
||||
private val homeServerLoginCompatibilityChecker: HomeServerLoginCompatibilityChecker,
|
||||
private val localNetworkPermissionAdvisor: LocalNetworkPermissionAdvisor,
|
||||
) {
|
||||
fun resolve(userInput: String): Flow<List<HomeserverData>> = flow {
|
||||
val flowContext = currentCoroutineContext()
|
||||
@@ -39,13 +41,17 @@ class HomeserverResolver(
|
||||
// Run all the requests in parallel
|
||||
withContext(dispatchers.io) {
|
||||
list.parallelMap { url ->
|
||||
val isValid = homeServerLoginCompatibilityChecker.check(url)
|
||||
.onFailure { Timber.w(it, "Failed to check compatibility with homeserver $url") }
|
||||
.getOrNull()
|
||||
?: return@parallelMap
|
||||
// Skip the compatibility probe if we'd need ACCESS_LOCAL_NETWORK first —
|
||||
// otherwise the probe hangs on the TCP timeout for ~30s. Emit the URL as a
|
||||
// candidate directly; the actual sign-in flow triggers the permission prompt.
|
||||
val shouldRequestPermissionOrIsValid = localNetworkPermissionAdvisor.shouldRequestPermissionFor(url) ||
|
||||
homeServerLoginCompatibilityChecker.check(url)
|
||||
.onFailure { Timber.w(it, "Failed to check compatibility with homeserver $url") }
|
||||
.getOrNull()
|
||||
?: return@parallelMap
|
||||
|
||||
// Emit the list as soon as possible
|
||||
if (isValid) {
|
||||
if (shouldRequestPermissionOrIsValid) {
|
||||
currentList.add(HomeserverData(homeserverUrl = url))
|
||||
withContext(flowContext) {
|
||||
emit(currentList.toList())
|
||||
|
||||
+17
-18
@@ -12,50 +12,49 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import dev.zacsweers.metro.Inject
|
||||
import io.element.android.appconfig.AuthenticationConfig
|
||||
import io.element.android.features.enterprise.api.EnterpriseService
|
||||
import io.element.android.features.login.impl.accountprovider.AccountProvider
|
||||
import io.element.android.features.login.impl.login.LoginHelper
|
||||
import io.element.android.features.login.impl.login.LoginModeEvent
|
||||
import io.element.android.features.login.impl.login.LoginModeState
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.core.uri.ensureProtocol
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Inject
|
||||
class ChooseAccountProviderPresenter(
|
||||
private val enterpriseService: EnterpriseService,
|
||||
private val loginHelper: LoginHelper,
|
||||
private val loginModePresenter: Presenter<LoginModeState>,
|
||||
) : Presenter<ChooseAccountProviderState> {
|
||||
@Composable
|
||||
override fun present(): ChooseAccountProviderState {
|
||||
val localCoroutineScope = rememberCoroutineScope()
|
||||
val loginMode by loginHelper.collectLoginMode()
|
||||
|
||||
val loginModeState = loginModePresenter.present()
|
||||
var selectedAccountProvider: AccountProvider? by remember { mutableStateOf(null) }
|
||||
|
||||
fun handleEvent(event: ChooseAccountProviderEvents) {
|
||||
when (event) {
|
||||
ChooseAccountProviderEvents.Continue -> localCoroutineScope.launch {
|
||||
selectedAccountProvider?.let {
|
||||
loginHelper.submit(
|
||||
isAccountCreation = false,
|
||||
homeserverUrl = it.url,
|
||||
resolvedHomeserverUrl = null,
|
||||
loginHint = null,
|
||||
ChooseAccountProviderEvents.Continue -> {
|
||||
selectedAccountProvider?.let { provider ->
|
||||
loginModeState.eventSink(
|
||||
LoginModeEvent.Submit(
|
||||
isAccountCreation = false,
|
||||
homeserverUrl = provider.url,
|
||||
resolvedHomeserverUrl = null,
|
||||
loginHint = null,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
is ChooseAccountProviderEvents.SelectAccountProvider -> {
|
||||
// Ensure that the user do not change the server during processing
|
||||
if (loginMode is AsyncData.Uninitialized) {
|
||||
// Ensure that the user does not change the server during processing.
|
||||
if (loginModeState.loginMode is AsyncData.Uninitialized) {
|
||||
selectedAccountProvider = event.accountProvider
|
||||
}
|
||||
}
|
||||
ChooseAccountProviderEvents.ClearError -> loginHelper.clearError()
|
||||
ChooseAccountProviderEvents.ClearError -> loginModeState.eventSink(LoginModeEvent.ClearError)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,7 +76,7 @@ class ChooseAccountProviderPresenter(
|
||||
return ChooseAccountProviderState(
|
||||
accountProviders = staticAccountProviderList,
|
||||
selectedAccountProvider = selectedAccountProvider,
|
||||
loginMode = loginMode,
|
||||
loginModeState = loginModeState,
|
||||
eventSink = ::handleEvent,
|
||||
)
|
||||
}
|
||||
|
||||
+4
-3
@@ -9,16 +9,17 @@
|
||||
package io.element.android.features.login.impl.screens.chooseaccountprovider
|
||||
|
||||
import io.element.android.features.login.impl.accountprovider.AccountProvider
|
||||
import io.element.android.features.login.impl.login.LoginMode
|
||||
import io.element.android.features.login.impl.login.LoginModeState
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
data class ChooseAccountProviderState(
|
||||
val accountProviders: ImmutableList<AccountProvider>,
|
||||
val selectedAccountProvider: AccountProvider?,
|
||||
val loginMode: AsyncData<LoginMode>,
|
||||
val loginModeState: LoginModeState,
|
||||
val eventSink: (ChooseAccountProviderEvents) -> Unit,
|
||||
) {
|
||||
val submitEnabled: Boolean
|
||||
get() = selectedAccountProvider != null && (loginMode is AsyncData.Uninitialized || loginMode is AsyncData.Loading)
|
||||
get() = selectedAccountProvider != null &&
|
||||
(loginModeState.loginMode is AsyncData.Uninitialized || loginModeState.loginMode is AsyncData.Loading)
|
||||
}
|
||||
|
||||
+5
-4
@@ -11,7 +11,8 @@ package io.element.android.features.login.impl.screens.chooseaccountprovider
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.features.login.impl.accountprovider.AccountProvider
|
||||
import io.element.android.features.login.impl.accountprovider.anAccountProvider
|
||||
import io.element.android.features.login.impl.login.LoginMode
|
||||
import io.element.android.features.login.impl.login.LoginModeState
|
||||
import io.element.android.features.login.impl.login.aLoginModeState
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
@@ -58,7 +59,7 @@ open class ChooseAccountProviderStateProvider : PreviewParameterProvider<ChooseA
|
||||
server3,
|
||||
),
|
||||
selectedAccountProvider = server2,
|
||||
loginMode = AsyncData.Loading(),
|
||||
loginModeState = aLoginModeState(loginMode = AsyncData.Loading()),
|
||||
),
|
||||
// Add other state here
|
||||
)
|
||||
@@ -69,11 +70,11 @@ fun aChooseAccountProviderState(
|
||||
anAccountProvider()
|
||||
),
|
||||
selectedAccountProvider: AccountProvider? = null,
|
||||
loginMode: AsyncData<LoginMode> = AsyncData.Uninitialized,
|
||||
loginModeState: LoginModeState = aLoginModeState(),
|
||||
eventSink: (ChooseAccountProviderEvents) -> Unit = {},
|
||||
) = ChooseAccountProviderState(
|
||||
accountProviders = accountProviders.toImmutableList(),
|
||||
selectedAccountProvider = selectedAccountProvider,
|
||||
loginMode = loginMode,
|
||||
loginModeState = loginModeState,
|
||||
eventSink = eventSink,
|
||||
)
|
||||
|
||||
+14
-3
@@ -33,6 +33,7 @@ import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||
import io.element.android.features.login.impl.R
|
||||
import io.element.android.features.login.impl.accountprovider.AccountProviderView
|
||||
import io.element.android.features.login.impl.login.LoginModeEvent
|
||||
import io.element.android.features.login.impl.login.LoginModeView
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
|
||||
@@ -44,6 +45,7 @@ import io.element.android.libraries.designsystem.theme.components.Button
|
||||
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||
import io.element.android.libraries.matrix.api.auth.OAuthDetails
|
||||
import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionDialogView
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
|
||||
@Composable
|
||||
@@ -56,9 +58,9 @@ fun ChooseAccountProviderView(
|
||||
onCreateAccountContinue: (url: String) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val isLoading by remember(state.loginMode) {
|
||||
val isLoading by remember(state.loginModeState.loginMode) {
|
||||
derivedStateOf {
|
||||
state.loginMode is AsyncData.Loading
|
||||
state.loginModeState.loginMode is AsyncData.Loading
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,7 +126,7 @@ fun ChooseAccountProviderView(
|
||||
Spacer(modifier = Modifier.height(48.dp))
|
||||
}
|
||||
LoginModeView(
|
||||
loginMode = state.loginMode,
|
||||
loginMode = state.loginModeState.loginMode,
|
||||
onClearError = {
|
||||
state.eventSink(ChooseAccountProviderEvents.ClearError)
|
||||
},
|
||||
@@ -135,6 +137,15 @@ fun ChooseAccountProviderView(
|
||||
)
|
||||
}
|
||||
}
|
||||
LocalNetworkPermissionDialogView(
|
||||
dialog = state.loginModeState.localNetworkPermissionDialog,
|
||||
onSubmit = {
|
||||
state.loginModeState.eventSink(LoginModeEvent.RequestLocalNetworkPermission)
|
||||
},
|
||||
onDismiss = {
|
||||
state.loginModeState.eventSink(LoginModeEvent.DismissLocalNetworkPermission)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
|
||||
+13
-10
@@ -20,7 +20,8 @@ import dev.zacsweers.metro.AssistedInject
|
||||
import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
|
||||
import io.element.android.features.login.impl.classic.ElementClassicConnection
|
||||
import io.element.android.features.login.impl.classic.ElementClassicConnectionState
|
||||
import io.element.android.features.login.impl.login.LoginHelper
|
||||
import io.element.android.features.login.impl.login.LoginModeEvent
|
||||
import io.element.android.features.login.impl.login.LoginModeState
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.core.meta.BuildMeta
|
||||
@@ -32,7 +33,7 @@ import kotlinx.coroutines.launch
|
||||
class LoginWithClassicPresenter(
|
||||
@Assisted private val userId: UserId,
|
||||
@Assisted private val navigator: LoginWithClassicNavigator,
|
||||
private val loginHelper: LoginHelper,
|
||||
private val loginModePresenter: Presenter<LoginModeState>,
|
||||
private val elementClassicConnection: ElementClassicConnection,
|
||||
private val accountProviderDataSource: AccountProviderDataSource,
|
||||
private val buildMeta: BuildMeta,
|
||||
@@ -51,7 +52,7 @@ class LoginWithClassicPresenter(
|
||||
var loginWithClassicAction by remember {
|
||||
mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized)
|
||||
}
|
||||
val loginMode by loginHelper.collectLoginMode()
|
||||
val loginModeState = loginModePresenter.present()
|
||||
val elementClassicConnectionState by elementClassicConnection.stateFlow.collectAsState()
|
||||
|
||||
fun handleEvent(event: LoginWithClassicEvent) {
|
||||
@@ -69,11 +70,13 @@ class LoginWithClassicPresenter(
|
||||
val elementClassicUserId = currentState.elementClassicSession.userId
|
||||
val accountProvider = elementClassicUserId.domainName.orEmpty().ensureProtocol()
|
||||
accountProviderDataSource.setUrl(accountProvider)
|
||||
loginHelper.submit(
|
||||
isAccountCreation = false,
|
||||
homeserverUrl = accountProvider,
|
||||
resolvedHomeserverUrl = currentState.elementClassicSession.homeserverUrl,
|
||||
loginHint = "mxid:" + elementClassicUserId.value,
|
||||
loginModeState.eventSink(
|
||||
LoginModeEvent.Submit(
|
||||
isAccountCreation = false,
|
||||
homeserverUrl = accountProvider,
|
||||
resolvedHomeserverUrl = currentState.elementClassicSession.homeserverUrl,
|
||||
loginHint = "mxid:" + elementClassicUserId.value,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -83,7 +86,7 @@ class LoginWithClassicPresenter(
|
||||
}
|
||||
LoginWithClassicEvent.ClearError -> {
|
||||
loginWithClassicAction = AsyncAction.Uninitialized
|
||||
loginHelper.clearError()
|
||||
loginModeState.eventSink(LoginModeEvent.ClearError)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -94,7 +97,7 @@ class LoginWithClassicPresenter(
|
||||
userId = userId,
|
||||
displayName = elementClassicReady?.displayName,
|
||||
avatar = elementClassicReady?.avatar,
|
||||
loginMode = loginMode,
|
||||
loginModeState = loginModeState,
|
||||
loginWithClassicAction = loginWithClassicAction,
|
||||
eventSink = ::handleEvent,
|
||||
)
|
||||
|
||||
+2
-3
@@ -9,9 +9,8 @@ package io.element.android.features.login.impl.screens.classic.loginwithclassic
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import androidx.compose.runtime.Stable
|
||||
import io.element.android.features.login.impl.login.LoginMode
|
||||
import io.element.android.features.login.impl.login.LoginModeState
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
|
||||
@Stable
|
||||
@@ -21,6 +20,6 @@ data class LoginWithClassicState(
|
||||
val displayName: String?,
|
||||
val avatar: Bitmap?,
|
||||
val loginWithClassicAction: AsyncAction<Unit>,
|
||||
val loginMode: AsyncData<LoginMode>,
|
||||
val loginModeState: LoginModeState,
|
||||
val eventSink: (LoginWithClassicEvent) -> Unit,
|
||||
)
|
||||
|
||||
+4
-4
@@ -9,9 +9,9 @@ package io.element.android.features.login.impl.screens.classic.loginwithclassic
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.features.login.impl.login.LoginMode
|
||||
import io.element.android.features.login.impl.login.LoginModeState
|
||||
import io.element.android.features.login.impl.login.aLoginModeState
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.designsystem.preview.USER_NAME_ALICE
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
|
||||
@@ -29,7 +29,7 @@ fun aLoginWithClassicState(
|
||||
displayName: String? = null,
|
||||
avatar: Bitmap? = null,
|
||||
loginWithClassicAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
||||
loginMode: AsyncData<LoginMode> = AsyncData.Uninitialized,
|
||||
loginModeState: LoginModeState = aLoginModeState(),
|
||||
eventSink: (LoginWithClassicEvent) -> Unit = {},
|
||||
) = LoginWithClassicState(
|
||||
isElementPro = isElementPro,
|
||||
@@ -37,6 +37,6 @@ fun aLoginWithClassicState(
|
||||
displayName = displayName,
|
||||
avatar = avatar,
|
||||
loginWithClassicAction = loginWithClassicAction,
|
||||
loginMode = loginMode,
|
||||
loginModeState = loginModeState,
|
||||
eventSink = eventSink,
|
||||
)
|
||||
|
||||
+14
-3
@@ -35,6 +35,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.features.login.impl.R
|
||||
import io.element.android.features.login.impl.login.LoginModeEvent
|
||||
import io.element.android.features.login.impl.login.LoginModeView
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule
|
||||
@@ -50,6 +51,7 @@ import io.element.android.libraries.designsystem.theme.components.Button
|
||||
import io.element.android.libraries.designsystem.theme.components.OutlinedButton
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.matrix.api.auth.OAuthDetails
|
||||
import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionDialogView
|
||||
import io.element.android.libraries.testtags.TestTags
|
||||
import io.element.android.libraries.testtags.testTag
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
@@ -65,9 +67,9 @@ fun LoginWithClassicView(
|
||||
onCreateAccountContinue: (url: String) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val isLoading by remember(state.loginMode) {
|
||||
val isLoading by remember(state.loginModeState.loginMode) {
|
||||
derivedStateOf {
|
||||
state.loginMode is AsyncData.Loading
|
||||
state.loginModeState.loginMode is AsyncData.Loading
|
||||
}
|
||||
}
|
||||
|
||||
@@ -195,7 +197,7 @@ fun LoginWithClassicView(
|
||||
}
|
||||
)
|
||||
LoginModeView(
|
||||
loginMode = state.loginMode,
|
||||
loginMode = state.loginModeState.loginMode,
|
||||
onClearError = {
|
||||
state.eventSink(LoginWithClassicEvent.ClearError)
|
||||
},
|
||||
@@ -204,6 +206,15 @@ fun LoginWithClassicView(
|
||||
onNeedLoginPassword = onNeedLoginPassword,
|
||||
onCreateAccountContinue = onCreateAccountContinue,
|
||||
)
|
||||
LocalNetworkPermissionDialogView(
|
||||
dialog = state.loginModeState.localNetworkPermissionDialog,
|
||||
onSubmit = {
|
||||
state.loginModeState.eventSink(LoginModeEvent.RequestLocalNetworkPermission)
|
||||
},
|
||||
onDismiss = {
|
||||
state.loginModeState.eventSink(LoginModeEvent.DismissLocalNetworkPermission)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
|
||||
+9
-12
@@ -11,20 +11,19 @@ package io.element.android.features.login.impl.screens.confirmaccountprovider
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.AssistedFactory
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
|
||||
import io.element.android.features.login.impl.login.LoginHelper
|
||||
import io.element.android.features.login.impl.login.LoginModeEvent
|
||||
import io.element.android.features.login.impl.login.LoginModeState
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@AssistedInject
|
||||
class ConfirmAccountProviderPresenter(
|
||||
@Assisted private val params: Params,
|
||||
private val accountProviderDataSource: AccountProviderDataSource,
|
||||
private val loginHelper: LoginHelper,
|
||||
private val loginModePresenter: Presenter<LoginModeState>,
|
||||
) : Presenter<ConfirmAccountProviderState> {
|
||||
data class Params(
|
||||
val isAccountCreation: Boolean,
|
||||
@@ -38,28 +37,26 @@ class ConfirmAccountProviderPresenter(
|
||||
@Composable
|
||||
override fun present(): ConfirmAccountProviderState {
|
||||
val accountProvider by accountProviderDataSource.flow.collectAsState()
|
||||
val localCoroutineScope = rememberCoroutineScope()
|
||||
|
||||
val loginMode by loginHelper.collectLoginMode()
|
||||
val loginModeState = loginModePresenter.present()
|
||||
|
||||
fun handleEvent(event: ConfirmAccountProviderEvents) {
|
||||
when (event) {
|
||||
ConfirmAccountProviderEvents.Continue -> localCoroutineScope.launch {
|
||||
loginHelper.submit(
|
||||
ConfirmAccountProviderEvents.Continue -> loginModeState.eventSink(
|
||||
LoginModeEvent.Submit(
|
||||
isAccountCreation = params.isAccountCreation,
|
||||
homeserverUrl = accountProvider.url,
|
||||
resolvedHomeserverUrl = null,
|
||||
loginHint = null,
|
||||
)
|
||||
}
|
||||
ConfirmAccountProviderEvents.ClearError -> loginHelper.clearError()
|
||||
)
|
||||
ConfirmAccountProviderEvents.ClearError -> loginModeState.eventSink(LoginModeEvent.ClearError)
|
||||
}
|
||||
}
|
||||
|
||||
return ConfirmAccountProviderState(
|
||||
accountProvider = accountProvider,
|
||||
isAccountCreation = params.isAccountCreation,
|
||||
loginMode = loginMode,
|
||||
loginModeState = loginModeState,
|
||||
eventSink = ::handleEvent,
|
||||
)
|
||||
}
|
||||
|
||||
+5
-3
@@ -9,14 +9,16 @@
|
||||
package io.element.android.features.login.impl.screens.confirmaccountprovider
|
||||
|
||||
import io.element.android.features.login.impl.accountprovider.AccountProvider
|
||||
import io.element.android.features.login.impl.login.LoginMode
|
||||
import io.element.android.features.login.impl.login.LoginModeState
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
|
||||
data class ConfirmAccountProviderState(
|
||||
val accountProvider: AccountProvider,
|
||||
val isAccountCreation: Boolean,
|
||||
val loginMode: AsyncData<LoginMode>,
|
||||
val loginModeState: LoginModeState,
|
||||
val eventSink: (ConfirmAccountProviderEvents) -> Unit
|
||||
) {
|
||||
val submitEnabled: Boolean get() = accountProvider.url.isNotEmpty() && (loginMode is AsyncData.Uninitialized || loginMode is AsyncData.Loading)
|
||||
val submitEnabled: Boolean
|
||||
get() = accountProvider.url.isNotEmpty() &&
|
||||
(loginModeState.loginMode is AsyncData.Uninitialized || loginModeState.loginMode is AsyncData.Loading)
|
||||
}
|
||||
|
||||
+5
-4
@@ -11,7 +11,8 @@ package io.element.android.features.login.impl.screens.confirmaccountprovider
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.features.login.impl.accountprovider.AccountProvider
|
||||
import io.element.android.features.login.impl.accountprovider.anAccountProvider
|
||||
import io.element.android.features.login.impl.login.LoginMode
|
||||
import io.element.android.features.login.impl.login.LoginModeState
|
||||
import io.element.android.features.login.impl.login.aLoginModeState
|
||||
import io.element.android.features.login.impl.screens.createaccount.AccountCreationNotSupported
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
|
||||
@@ -24,7 +25,7 @@ open class ConfirmAccountProviderStateProvider : PreviewParameterProvider<Confir
|
||||
),
|
||||
aConfirmAccountProviderState(
|
||||
isAccountCreation = true,
|
||||
loginMode = AsyncData.Failure(AccountCreationNotSupported())
|
||||
loginModeState = aLoginModeState(loginMode = AsyncData.Failure(AccountCreationNotSupported())),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -32,11 +33,11 @@ open class ConfirmAccountProviderStateProvider : PreviewParameterProvider<Confir
|
||||
private fun aConfirmAccountProviderState(
|
||||
accountProvider: AccountProvider = anAccountProvider(),
|
||||
isAccountCreation: Boolean = false,
|
||||
loginMode: AsyncData<LoginMode> = AsyncData.Uninitialized,
|
||||
loginModeState: LoginModeState = aLoginModeState(),
|
||||
eventSink: (ConfirmAccountProviderEvents) -> Unit = {},
|
||||
) = ConfirmAccountProviderState(
|
||||
accountProvider = accountProvider,
|
||||
isAccountCreation = isAccountCreation,
|
||||
loginMode = loginMode,
|
||||
loginModeState = loginModeState,
|
||||
eventSink = eventSink
|
||||
)
|
||||
|
||||
+15
-3
@@ -20,6 +20,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||
import io.element.android.features.login.impl.R
|
||||
import io.element.android.features.login.impl.login.LoginModeEvent
|
||||
import io.element.android.features.login.impl.login.LoginModeView
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule
|
||||
@@ -31,6 +32,7 @@ import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.theme.components.Button
|
||||
import io.element.android.libraries.designsystem.theme.components.TextButton
|
||||
import io.element.android.libraries.matrix.api.auth.OAuthDetails
|
||||
import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionDialogView
|
||||
import io.element.android.libraries.testtags.TestTags
|
||||
import io.element.android.libraries.testtags.testTag
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
@@ -45,9 +47,9 @@ fun ConfirmAccountProviderView(
|
||||
onChange: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val isLoading by remember(state.loginMode) {
|
||||
val isLoading by remember(state.loginModeState.loginMode) {
|
||||
derivedStateOf {
|
||||
state.loginMode is AsyncData.Loading
|
||||
state.loginModeState.loginMode is AsyncData.Loading
|
||||
}
|
||||
}
|
||||
val eventSink = state.eventSink
|
||||
@@ -98,7 +100,7 @@ fun ConfirmAccountProviderView(
|
||||
}
|
||||
) {
|
||||
LoginModeView(
|
||||
loginMode = state.loginMode,
|
||||
loginMode = state.loginModeState.loginMode,
|
||||
onClearError = {
|
||||
eventSink(ConfirmAccountProviderEvents.ClearError)
|
||||
},
|
||||
@@ -108,6 +110,16 @@ fun ConfirmAccountProviderView(
|
||||
onCreateAccountContinue = onCreateAccountContinue,
|
||||
)
|
||||
}
|
||||
|
||||
LocalNetworkPermissionDialogView(
|
||||
dialog = state.loginModeState.localNetworkPermissionDialog,
|
||||
onSubmit = {
|
||||
state.loginModeState.eventSink(LoginModeEvent.RequestLocalNetworkPermission)
|
||||
},
|
||||
onDismiss = {
|
||||
state.loginModeState.eventSink(LoginModeEvent.DismissLocalNetworkPermission)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
|
||||
+13
-10
@@ -25,7 +25,8 @@ import io.element.android.features.enterprise.api.EnterpriseService
|
||||
import io.element.android.features.enterprise.api.canConnectToAnyHomeserver
|
||||
import io.element.android.features.login.impl.accesscontrol.DefaultAccountProviderAccessControl
|
||||
import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
|
||||
import io.element.android.features.login.impl.login.LoginHelper
|
||||
import io.element.android.features.login.impl.login.LoginModeEvent
|
||||
import io.element.android.features.login.impl.login.LoginModeState
|
||||
import io.element.android.features.rageshake.api.RageshakeFeatureAvailability
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.core.meta.BuildMeta
|
||||
@@ -41,7 +42,7 @@ class OnBoardingPresenter(
|
||||
private val enterpriseService: EnterpriseService,
|
||||
private val defaultAccountProviderAccessControl: DefaultAccountProviderAccessControl,
|
||||
private val rageshakeFeatureAvailability: RageshakeFeatureAvailability,
|
||||
private val loginHelper: LoginHelper,
|
||||
private val loginModePresenter: Presenter<LoginModeState>,
|
||||
private val onBoardingLogoResIdProvider: OnBoardingLogoResIdProvider,
|
||||
private val sessionStore: SessionStore,
|
||||
private val accountProviderDataSource: AccountProviderDataSource,
|
||||
@@ -98,21 +99,23 @@ class OnBoardingPresenter(
|
||||
value = sessionStore.numberOfSessions() > 0
|
||||
}
|
||||
|
||||
val loginMode by loginHelper.collectLoginMode()
|
||||
val loginModeState = loginModePresenter.present()
|
||||
|
||||
fun handleEvent(event: OnBoardingEvents) {
|
||||
when (event) {
|
||||
is OnBoardingEvents.OnSignIn -> localCoroutineScope.launch {
|
||||
// Ensure that the current account provider is set
|
||||
accountProviderDataSource.setUrl(event.defaultAccountProvider)
|
||||
loginHelper.submit(
|
||||
isAccountCreation = false,
|
||||
homeserverUrl = event.defaultAccountProvider,
|
||||
resolvedHomeserverUrl = null,
|
||||
loginHint = params.loginHint?.takeIf { forcedAccountProvider == null },
|
||||
loginModeState.eventSink(
|
||||
LoginModeEvent.Submit(
|
||||
isAccountCreation = false,
|
||||
homeserverUrl = event.defaultAccountProvider,
|
||||
resolvedHomeserverUrl = null,
|
||||
loginHint = params.loginHint?.takeIf { forcedAccountProvider == null },
|
||||
)
|
||||
)
|
||||
}
|
||||
OnBoardingEvents.ClearError -> loginHelper.clearError()
|
||||
OnBoardingEvents.ClearError -> loginModeState.eventSink(LoginModeEvent.ClearError)
|
||||
OnBoardingEvents.OnVersionClick -> {
|
||||
if (canReportBug) {
|
||||
if (multipleTapToUnlock.unlock(localCoroutineScope)) {
|
||||
@@ -133,7 +136,7 @@ class OnBoardingPresenter(
|
||||
canLoginWithQrCode = canLoginWithQrCode,
|
||||
canCreateAccount = defaultAccountProvider == null && canConnectToAnyHomeserver && OnBoardingConfig.CAN_CREATE_ACCOUNT,
|
||||
canReportBug = canReportBug && showReportBug,
|
||||
loginMode = loginMode,
|
||||
loginModeState = loginModeState,
|
||||
version = buildMeta.versionName,
|
||||
onBoardingLogoResId = onBoardingLogoResId,
|
||||
eventSink = ::handleEvent,
|
||||
|
||||
+4
-3
@@ -9,7 +9,7 @@
|
||||
package io.element.android.features.login.impl.screens.onboarding
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import io.element.android.features.login.impl.login.LoginMode
|
||||
import io.element.android.features.login.impl.login.LoginModeState
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
|
||||
data class OnBoardingState(
|
||||
@@ -25,9 +25,10 @@ data class OnBoardingState(
|
||||
val version: String,
|
||||
@DrawableRes
|
||||
val onBoardingLogoResId: Int?,
|
||||
val loginMode: AsyncData<LoginMode>,
|
||||
val loginModeState: LoginModeState,
|
||||
val eventSink: (OnBoardingEvents) -> Unit,
|
||||
) {
|
||||
val submitEnabled: Boolean
|
||||
get() = defaultAccountProvider != null && (loginMode is AsyncData.Uninitialized || loginMode is AsyncData.Loading)
|
||||
get() = defaultAccountProvider != null &&
|
||||
(loginModeState.loginMode is AsyncData.Uninitialized || loginModeState.loginMode is AsyncData.Loading)
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user