Merge branch 'release/25.10.0' into main
This commit is contained in:
commit
5246dc0aba
1593 changed files with 20683 additions and 4573 deletions
|
|
@ -913,3 +913,8 @@ ij_yaml_sequence_on_new_line = false
|
||||||
ij_yaml_space_before_colon = false
|
ij_yaml_space_before_colon = false
|
||||||
ij_yaml_spaces_within_braces = true
|
ij_yaml_spaces_within_braces = true
|
||||||
ij_yaml_spaces_within_brackets = true
|
ij_yaml_spaces_within_brackets = true
|
||||||
|
|
||||||
|
[**/generated/**]
|
||||||
|
generated_code = true
|
||||||
|
ij_formatter_enabled = false
|
||||||
|
ktlint = disabled
|
||||||
1
.gitattributes
vendored
1
.gitattributes
vendored
|
|
@ -1,4 +1,5 @@
|
||||||
screenshots/**/*.png filter=lfs diff=lfs merge=lfs -text
|
screenshots/**/*.png filter=lfs diff=lfs merge=lfs -text
|
||||||
|
libraries/compound/screenshots/** filter=lfs diff=lfs merge=lfs -text
|
||||||
**/snapshots/**/*.png filter=lfs diff=lfs merge=lfs -text
|
**/snapshots/**/*.png filter=lfs diff=lfs merge=lfs -text
|
||||||
**/docs/images-lfs/*.png filter=lfs diff=lfs merge=lfs -text
|
**/docs/images-lfs/*.png filter=lfs diff=lfs merge=lfs -text
|
||||||
libraries/mediaupload/impl/src/test/assets/* filter=lfs diff=lfs merge=lfs -text
|
libraries/mediaupload/impl/src/test/assets/* filter=lfs diff=lfs merge=lfs -text
|
||||||
|
|
|
||||||
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
|
|
@ -36,7 +36,7 @@ jobs:
|
||||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
java-version: '21'
|
java-version: '21'
|
||||||
- name: Configure gradle
|
- name: Configure gradle
|
||||||
uses: gradle/actions/setup-gradle@v4
|
uses: gradle/actions/setup-gradle@v5
|
||||||
with:
|
with:
|
||||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||||
- name: Assemble debug APKs
|
- name: Assemble debug APKs
|
||||||
|
|
|
||||||
2
.github/workflows/build_enterprise.yml
vendored
2
.github/workflows/build_enterprise.yml
vendored
|
|
@ -44,7 +44,7 @@ jobs:
|
||||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
java-version: '21'
|
java-version: '21'
|
||||||
- name: Configure gradle
|
- name: Configure gradle
|
||||||
uses: gradle/actions/setup-gradle@v4
|
uses: gradle/actions/setup-gradle@v5
|
||||||
with:
|
with:
|
||||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||||
- name: Assemble debug Gplay Enterprise APK
|
- name: Assemble debug Gplay Enterprise APK
|
||||||
|
|
|
||||||
2
.github/workflows/generate_github_pages.yml
vendored
2
.github/workflows/generate_github_pages.yml
vendored
|
|
@ -19,7 +19,7 @@ jobs:
|
||||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
java-version: '21'
|
java-version: '21'
|
||||||
- name: Configure gradle
|
- name: Configure gradle
|
||||||
uses: gradle/actions/setup-gradle@v4
|
uses: gradle/actions/setup-gradle@v5
|
||||||
with:
|
with:
|
||||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||||
- name: Set up Python 3.12
|
- name: Set up Python 3.12
|
||||||
|
|
|
||||||
2
.github/workflows/maestro-local.yml
vendored
2
.github/workflows/maestro-local.yml
vendored
|
|
@ -34,7 +34,7 @@ jobs:
|
||||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
java-version: '21'
|
java-version: '21'
|
||||||
- name: Configure gradle
|
- name: Configure gradle
|
||||||
uses: gradle/actions/setup-gradle@v4
|
uses: gradle/actions/setup-gradle@v5
|
||||||
with:
|
with:
|
||||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||||
- name: Assemble debug APK
|
- name: Assemble debug APK
|
||||||
|
|
|
||||||
4
.github/workflows/nightlyReports.yml
vendored
4
.github/workflows/nightlyReports.yml
vendored
|
|
@ -27,7 +27,7 @@ jobs:
|
||||||
java-version: '21'
|
java-version: '21'
|
||||||
|
|
||||||
- name: Configure gradle
|
- name: Configure gradle
|
||||||
uses: gradle/actions/setup-gradle@v4
|
uses: gradle/actions/setup-gradle@v5
|
||||||
with:
|
with:
|
||||||
cache-read-only: false
|
cache-read-only: false
|
||||||
|
|
||||||
|
|
@ -67,7 +67,7 @@ jobs:
|
||||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
java-version: '21'
|
java-version: '21'
|
||||||
- name: Configure gradle
|
- name: Configure gradle
|
||||||
uses: gradle/actions/setup-gradle@v4
|
uses: gradle/actions/setup-gradle@v5
|
||||||
with:
|
with:
|
||||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||||
- name: Dependency analysis
|
- name: Dependency analysis
|
||||||
|
|
|
||||||
12
.github/workflows/quality.yml
vendored
12
.github/workflows/quality.yml
vendored
|
|
@ -52,7 +52,7 @@ jobs:
|
||||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
java-version: '21'
|
java-version: '21'
|
||||||
- name: Configure gradle
|
- name: Configure gradle
|
||||||
uses: gradle/actions/setup-gradle@v4
|
uses: gradle/actions/setup-gradle@v5
|
||||||
with:
|
with:
|
||||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||||
- name: Set up Python 3.12
|
- name: Set up Python 3.12
|
||||||
|
|
@ -90,7 +90,7 @@ jobs:
|
||||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
java-version: '21'
|
java-version: '21'
|
||||||
- name: Configure gradle
|
- name: Configure gradle
|
||||||
uses: gradle/actions/setup-gradle@v4
|
uses: gradle/actions/setup-gradle@v5
|
||||||
with:
|
with:
|
||||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||||
- name: Run Konsist tests
|
- name: Run Konsist tests
|
||||||
|
|
@ -130,7 +130,7 @@ jobs:
|
||||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
java-version: '21'
|
java-version: '21'
|
||||||
- name: Configure gradle
|
- name: Configure gradle
|
||||||
uses: gradle/actions/setup-gradle@v4
|
uses: gradle/actions/setup-gradle@v5
|
||||||
with:
|
with:
|
||||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||||
- name: Build Gplay Debug
|
- name: Build Gplay Debug
|
||||||
|
|
@ -174,7 +174,7 @@ jobs:
|
||||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
java-version: '21'
|
java-version: '21'
|
||||||
- name: Configure gradle
|
- name: Configure gradle
|
||||||
uses: gradle/actions/setup-gradle@v4
|
uses: gradle/actions/setup-gradle@v5
|
||||||
with:
|
with:
|
||||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||||
- name: Run Detekt
|
- name: Run Detekt
|
||||||
|
|
@ -214,7 +214,7 @@ jobs:
|
||||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
java-version: '21'
|
java-version: '21'
|
||||||
- name: Configure gradle
|
- name: Configure gradle
|
||||||
uses: gradle/actions/setup-gradle@v4
|
uses: gradle/actions/setup-gradle@v5
|
||||||
with:
|
with:
|
||||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||||
- name: Run Ktlint check
|
- name: Run Ktlint check
|
||||||
|
|
@ -254,7 +254,7 @@ jobs:
|
||||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
java-version: '21'
|
java-version: '21'
|
||||||
- name: Configure gradle
|
- name: Configure gradle
|
||||||
uses: gradle/actions/setup-gradle@v4
|
uses: gradle/actions/setup-gradle@v5
|
||||||
with:
|
with:
|
||||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||||
- name: Run Knit
|
- name: Run Knit
|
||||||
|
|
|
||||||
2
.github/workflows/recordScreenshots.yml
vendored
2
.github/workflows/recordScreenshots.yml
vendored
|
|
@ -40,7 +40,7 @@ jobs:
|
||||||
java-version: '21'
|
java-version: '21'
|
||||||
# Add gradle cache, this should speed up the process
|
# Add gradle cache, this should speed up the process
|
||||||
- name: Configure gradle
|
- name: Configure gradle
|
||||||
uses: gradle/actions/setup-gradle@v4
|
uses: gradle/actions/setup-gradle@v5
|
||||||
with:
|
with:
|
||||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||||
- name: Record screenshots
|
- name: Record screenshots
|
||||||
|
|
|
||||||
6
.github/workflows/release.yml
vendored
6
.github/workflows/release.yml
vendored
|
|
@ -25,7 +25,7 @@ jobs:
|
||||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
java-version: '21'
|
java-version: '21'
|
||||||
- name: Configure gradle
|
- name: Configure gradle
|
||||||
uses: gradle/actions/setup-gradle@v4
|
uses: gradle/actions/setup-gradle@v5
|
||||||
- name: Create app bundle
|
- name: Create app bundle
|
||||||
env:
|
env:
|
||||||
ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }}
|
ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }}
|
||||||
|
|
@ -66,7 +66,7 @@ jobs:
|
||||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
java-version: '21'
|
java-version: '21'
|
||||||
- name: Configure gradle
|
- name: Configure gradle
|
||||||
uses: gradle/actions/setup-gradle@v4
|
uses: gradle/actions/setup-gradle@v5
|
||||||
- name: Create Enterprise app bundle
|
- name: Create Enterprise app bundle
|
||||||
env:
|
env:
|
||||||
ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }}
|
ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }}
|
||||||
|
|
@ -94,7 +94,7 @@ jobs:
|
||||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
java-version: '21'
|
java-version: '21'
|
||||||
- name: Configure gradle
|
- name: Configure gradle
|
||||||
uses: gradle/actions/setup-gradle@v4
|
uses: gradle/actions/setup-gradle@v5
|
||||||
- name: Create APKs
|
- name: Create APKs
|
||||||
env:
|
env:
|
||||||
ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }}
|
ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }}
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,12 @@ echo "Deleting previous screenshots"
|
||||||
echo "Record screenshots"
|
echo "Record screenshots"
|
||||||
./gradlew recordPaparazziDebug --stacktrace $GRADLE_ARGS
|
./gradlew recordPaparazziDebug --stacktrace $GRADLE_ARGS
|
||||||
|
|
||||||
|
echo "Deleting previous screenshots"
|
||||||
|
./gradlew removeOldScreenshots --stacktrace --warn $GRADLE_ARGS
|
||||||
|
|
||||||
|
echo "Record screenshots (Compound)"
|
||||||
|
./gradlew :libraries:compound:recordRoborazziDebug --stacktrace -PpreDexEnable=false --max-workers 4 --warn $GRADLE_ARGS
|
||||||
|
|
||||||
echo "Committing changes"
|
echo "Committing changes"
|
||||||
git config http.sslVerify false
|
git config http.sslVerify false
|
||||||
|
|
||||||
|
|
|
||||||
2
.github/workflows/sonar.yml
vendored
2
.github/workflows/sonar.yml
vendored
|
|
@ -33,7 +33,7 @@ jobs:
|
||||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
java-version: '21'
|
java-version: '21'
|
||||||
- name: Configure gradle
|
- name: Configure gradle
|
||||||
uses: gradle/actions/setup-gradle@v4
|
uses: gradle/actions/setup-gradle@v5
|
||||||
with:
|
with:
|
||||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||||
- name: Build debug code and test fixtures
|
- name: Build debug code and test fixtures
|
||||||
|
|
|
||||||
2
.github/workflows/sync-localazy.yml
vendored
2
.github/workflows/sync-localazy.yml
vendored
|
|
@ -18,7 +18,7 @@ jobs:
|
||||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
java-version: '21'
|
java-version: '21'
|
||||||
- name: Configure gradle
|
- name: Configure gradle
|
||||||
uses: gradle/actions/setup-gradle@v4
|
uses: gradle/actions/setup-gradle@v5
|
||||||
with:
|
with:
|
||||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||||
- name: Set up Python 3.12
|
- name: Set up Python 3.12
|
||||||
|
|
|
||||||
3
.github/workflows/tests.yml
vendored
3
.github/workflows/tests.yml
vendored
|
|
@ -52,7 +52,7 @@ jobs:
|
||||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
java-version: '21'
|
java-version: '21'
|
||||||
- name: Configure gradle
|
- name: Configure gradle
|
||||||
uses: gradle/actions/setup-gradle@v4
|
uses: gradle/actions/setup-gradle@v5
|
||||||
with:
|
with:
|
||||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||||
|
|
||||||
|
|
@ -78,6 +78,7 @@ jobs:
|
||||||
name: tests-and-screenshot-tests-results
|
name: tests-and-screenshot-tests-results
|
||||||
path: |
|
path: |
|
||||||
**/build/paparazzi/failures/
|
**/build/paparazzi/failures/
|
||||||
|
**/build/roborazzi/failures/
|
||||||
**/build/reports/tests/*UnitTest/
|
**/build/reports/tests/*UnitTest/
|
||||||
|
|
||||||
# https://github.com/codecov/codecov-action
|
# https://github.com/codecov/codecov-action
|
||||||
|
|
|
||||||
2
.idea/kotlinc.xml
generated
2
.idea/kotlinc.xml
generated
|
|
@ -1,6 +1,6 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="KotlinJpsPluginSettings">
|
<component name="KotlinJpsPluginSettings">
|
||||||
<option name="version" value="2.2.10" />
|
<option name="version" value="2.2.20" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
|
|
@ -8,6 +8,6 @@ appId: ${MAESTRO_APP_ID}
|
||||||
- hideKeyboard
|
- hideKeyboard
|
||||||
- tapOn: "Continue"
|
- tapOn: "Continue"
|
||||||
- extendedWaitUntil:
|
- extendedWaitUntil:
|
||||||
visible: "Verification complete"
|
visible: "Device verified"
|
||||||
timeout: 30000
|
timeout: 30000
|
||||||
- tapOn: "Continue"
|
- tapOn: "Continue"
|
||||||
|
|
|
||||||
70
CHANGES.md
70
CHANGES.md
|
|
@ -1,3 +1,73 @@
|
||||||
|
Changes in Element X v25.09.2
|
||||||
|
=============================
|
||||||
|
|
||||||
|
## What's Changed
|
||||||
|
### ✨ Features
|
||||||
|
* Show progress dialog while we are sending invites in a room by @richvdh in https://github.com/element-hq/element-x-android/pull/5342
|
||||||
|
* Call: RTC decline event support by @BillCarsonFr in https://github.com/element-hq/element-x-android/pull/5305
|
||||||
|
* Add room info to the thread's top app bar by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5374
|
||||||
|
### 🙌 Improvements
|
||||||
|
* Use the new RtcNotification event instead of the now deprecated CallNotify by @BillCarsonFr in https://github.com/element-hq/element-x-android/pull/5357
|
||||||
|
### 🐛 Bugfixes
|
||||||
|
* Increase Element Call audio init delay ensuring the right audio device is used by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5315
|
||||||
|
* Do not center the dialog title text for dialogs with no icon by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5332
|
||||||
|
* Media viewer: release the `ExoPlayers` when the hosting composables are disposed by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5351
|
||||||
|
* Make PushData.clientSecret mandatory. by @bmarty in https://github.com/element-hq/element-x-android/pull/5369
|
||||||
|
* Cleanup ftue code and ensure verification confirmation is displayed by @bmarty in https://github.com/element-hq/element-x-android/pull/5379
|
||||||
|
* Change in clear cache behavior by @bmarty in https://github.com/element-hq/element-x-android/pull/5388
|
||||||
|
* fix (room navigation) : fix navigation when leaving room/space by @ganfra in https://github.com/element-hq/element-x-android/pull/5376
|
||||||
|
* fix (timeline) : forward pagination regression by @ganfra in https://github.com/element-hq/element-x-android/pull/5389
|
||||||
|
* When joining a call, wait for the `content_loaded` action by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5399
|
||||||
|
* Ensure the thread summary sender's display name won't wrap to the next line by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5403
|
||||||
|
### 🗣 Translations
|
||||||
|
* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/5349
|
||||||
|
* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/5385
|
||||||
|
### 🧱 Build
|
||||||
|
* Improve release script and the file Versions.kt by @bmarty in https://github.com/element-hq/element-x-android/pull/5318
|
||||||
|
* Dependency: extract the Matrix SDK and add instructions for upgrading the library by @bmarty in https://github.com/element-hq/element-x-android/pull/5363
|
||||||
|
* Add test on DefaultSpaceEntryPoint by @bmarty in https://github.com/element-hq/element-x-android/pull/5343
|
||||||
|
### 🚧 In development 🚧
|
||||||
|
* Space list by @bmarty in https://github.com/element-hq/element-x-android/pull/5320
|
||||||
|
* Feature : Join Space (WIP) by @ganfra in https://github.com/element-hq/element-x-android/pull/5378
|
||||||
|
### Dependency upgrades
|
||||||
|
* Update activity to v1.11.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5324
|
||||||
|
* Update dependency com.google.truth:truth to v1.4.5 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5322
|
||||||
|
* Update dependency io.sentry:sentry-android to v8.21.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5310
|
||||||
|
* Update dependency org.matrix.rustcomponents:sdk-android to v25.9.10 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5323
|
||||||
|
* Update dependency androidx.sqlite:sqlite-ktx to v2.6.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5337
|
||||||
|
* Update camera to v1.5.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5336
|
||||||
|
* Update dependency com.posthog:posthog-android to v3.21.2 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5333
|
||||||
|
* Update dependency com.google.testparameterinjector:test-parameter-injector to v1.19 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5341
|
||||||
|
* Upgrade Rust SDK bindings to v25.09.15 by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5353
|
||||||
|
* Update dependency org.matrix.rustcomponents:sdk-android to v25.9.16 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5359
|
||||||
|
* Update dependency org.matrix.rustcomponents:sdk-android to v25.9.18 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5365
|
||||||
|
* Update telephoto to v0.17.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5350
|
||||||
|
* Update dependency org.matrix.rustcomponents:sdk-android to v25.9.19 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5377
|
||||||
|
* Update dependency com.google.firebase:firebase-bom to v34.3.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5367
|
||||||
|
* Upgrade Element Call embedded dependency to `v0.16.0-rc.4` by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5391
|
||||||
|
* Update dependencyAnalysis to v3 (major) by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5194
|
||||||
|
* Update dependency org.maplibre.gl:android-sdk to v11.13.5 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5381
|
||||||
|
* Update dependency org.matrix.rustcomponents:sdk-android to v25.9.23 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5396
|
||||||
|
* Update plugin dependencycheck to v12.1.5 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5382
|
||||||
|
* Update dependency io.sentry:sentry-android to v8.22.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5397
|
||||||
|
### Others
|
||||||
|
* Cleanup nodes by @bmarty in https://github.com/element-hq/element-x-android/pull/5358
|
||||||
|
* Complete test on MediaGalleryPresenter by @bmarty in https://github.com/element-hq/element-x-android/pull/5361
|
||||||
|
* Remove dead code by @bmarty in https://github.com/element-hq/element-x-android/pull/5306
|
||||||
|
* Introduce BugReportFlowNode, and remove NavTarget.ViewLogs from RootFlowNode by @bmarty in https://github.com/element-hq/element-x-android/pull/5370
|
||||||
|
* When logging out from Pin code screen, logout from all the sessions. by @bmarty in https://github.com/element-hq/element-x-android/pull/5372
|
||||||
|
* Clean MatrixAuthenticationService and SessionStore API by @bmarty in https://github.com/element-hq/element-x-android/pull/5371
|
||||||
|
* Add logs to detect duplicates in the room list by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5364
|
||||||
|
* Add troubleshoot notification test about blocked users by @bmarty in https://github.com/element-hq/element-x-android/pull/5394
|
||||||
|
* Add thread decoration with latest event details by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5355
|
||||||
|
* Rework on messages view top bars by @bmarty in https://github.com/element-hq/element-x-android/pull/5401
|
||||||
|
* Put developer settings at the end of the view by @p1gp1g in https://github.com/element-hq/element-x-android/pull/5387
|
||||||
|
|
||||||
|
## New Contributors
|
||||||
|
* @p1gp1g made their first contribution in https://github.com/element-hq/element-x-android/pull/5387
|
||||||
|
|
||||||
|
**Full Changelog**: https://github.com/element-hq/element-x-android/compare/v25.09.1...v25.09.2
|
||||||
|
|
||||||
Changes in Element X v25.09.1
|
Changes in Element X v25.09.1
|
||||||
=============================
|
=============================
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,3 @@ plugins {
|
||||||
alias(libs.plugins.kotlin.jvm)
|
alias(libs.plugins.kotlin.jvm)
|
||||||
id("com.android.lint")
|
id("com.android.lint")
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
|
||||||
api(libs.inject)
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,6 @@ import io.element.android.x.BuildConfig
|
||||||
import io.element.android.x.R
|
import io.element.android.x.R
|
||||||
import kotlinx.coroutines.CoroutineName
|
import kotlinx.coroutines.CoroutineName
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.MainScope
|
import kotlinx.coroutines.MainScope
|
||||||
import kotlinx.coroutines.plus
|
import kotlinx.coroutines.plus
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
@ -107,11 +106,7 @@ object AppModule {
|
||||||
@Provides
|
@Provides
|
||||||
@SingleIn(AppScope::class)
|
@SingleIn(AppScope::class)
|
||||||
fun providesCoroutineDispatchers(): CoroutineDispatchers {
|
fun providesCoroutineDispatchers(): CoroutineDispatchers {
|
||||||
return CoroutineDispatchers(
|
return CoroutineDispatchers.Default
|
||||||
io = Dispatchers.IO,
|
|
||||||
computation = Dispatchers.Default,
|
|
||||||
main = Dispatchers.Main,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
|
|
|
||||||
|
|
@ -9,15 +9,15 @@ package io.element.android.x.di
|
||||||
|
|
||||||
import dev.zacsweers.metro.ContributesBinding
|
import dev.zacsweers.metro.ContributesBinding
|
||||||
import dev.zacsweers.metro.Inject
|
import dev.zacsweers.metro.Inject
|
||||||
import io.element.android.appnav.di.RoomComponentFactory
|
import io.element.android.appnav.di.RoomGraphFactory
|
||||||
import io.element.android.libraries.di.SessionScope
|
import io.element.android.libraries.di.SessionScope
|
||||||
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
||||||
|
|
||||||
@ContributesBinding(SessionScope::class)
|
@ContributesBinding(SessionScope::class)
|
||||||
@Inject
|
@Inject
|
||||||
class DefaultRoomComponentFactory(
|
class DefaultRoomGraphFactory(
|
||||||
private val sessionGraph: SessionGraph,
|
private val sessionGraph: SessionGraph,
|
||||||
) : RoomComponentFactory {
|
) : RoomGraphFactory {
|
||||||
override fun create(room: JoinedRoom): Any {
|
override fun create(room: JoinedRoom): Any {
|
||||||
return sessionGraph.roomGraphFactory
|
return sessionGraph.roomGraphFactory
|
||||||
.create(room, room)
|
.create(room, room)
|
||||||
|
|
@ -7,18 +7,15 @@
|
||||||
|
|
||||||
package io.element.android.x.di
|
package io.element.android.x.di
|
||||||
|
|
||||||
import dev.zacsweers.metro.ContributesTo
|
|
||||||
import dev.zacsweers.metro.GraphExtension
|
import dev.zacsweers.metro.GraphExtension
|
||||||
import dev.zacsweers.metro.Provides
|
import dev.zacsweers.metro.Provides
|
||||||
import io.element.android.libraries.architecture.NodeFactoriesBindings
|
import io.element.android.libraries.architecture.NodeFactoriesBindings
|
||||||
import io.element.android.libraries.di.RoomScope
|
import io.element.android.libraries.di.RoomScope
|
||||||
import io.element.android.libraries.di.SessionScope
|
|
||||||
import io.element.android.libraries.matrix.api.room.BaseRoom
|
import io.element.android.libraries.matrix.api.room.BaseRoom
|
||||||
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
||||||
|
|
||||||
@GraphExtension(RoomScope::class)
|
@GraphExtension(RoomScope::class)
|
||||||
interface RoomGraph : NodeFactoriesBindings {
|
interface RoomGraph : NodeFactoriesBindings {
|
||||||
@ContributesTo(SessionScope::class)
|
|
||||||
@GraphExtension.Factory
|
@GraphExtension.Factory
|
||||||
interface Factory {
|
interface Factory {
|
||||||
fun create(
|
fun create(
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,6 @@
|
||||||
|
|
||||||
package io.element.android.x.di
|
package io.element.android.x.di
|
||||||
|
|
||||||
import dev.zacsweers.metro.AppScope
|
|
||||||
import dev.zacsweers.metro.ContributesTo
|
|
||||||
import dev.zacsweers.metro.GraphExtension
|
import dev.zacsweers.metro.GraphExtension
|
||||||
import dev.zacsweers.metro.Provides
|
import dev.zacsweers.metro.Provides
|
||||||
import io.element.android.libraries.architecture.NodeFactoriesBindings
|
import io.element.android.libraries.architecture.NodeFactoriesBindings
|
||||||
|
|
@ -19,7 +17,6 @@ import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
interface SessionGraph : NodeFactoriesBindings {
|
interface SessionGraph : NodeFactoriesBindings {
|
||||||
val roomGraphFactory: RoomGraph.Factory
|
val roomGraphFactory: RoomGraph.Factory
|
||||||
|
|
||||||
@ContributesTo(AppScope::class)
|
|
||||||
@GraphExtension.Factory
|
@GraphExtension.Factory
|
||||||
interface Factory {
|
interface Factory {
|
||||||
fun create(@Provides matrixClient: MatrixClient): SessionGraph
|
fun create(@Provides matrixClient: MatrixClient): SessionGraph
|
||||||
|
|
|
||||||
|
|
@ -26,9 +26,11 @@ dependencies {
|
||||||
allFeaturesApi(project)
|
allFeaturesApi(project)
|
||||||
|
|
||||||
implementation(projects.libraries.core)
|
implementation(projects.libraries.core)
|
||||||
|
implementation(projects.libraries.accountselect.api)
|
||||||
implementation(projects.libraries.androidutils)
|
implementation(projects.libraries.androidutils)
|
||||||
implementation(projects.libraries.architecture)
|
implementation(projects.libraries.architecture)
|
||||||
implementation(projects.libraries.deeplink.api)
|
implementation(projects.libraries.deeplink.api)
|
||||||
|
implementation(projects.libraries.featureflag.api)
|
||||||
implementation(projects.libraries.matrix.api)
|
implementation(projects.libraries.matrix.api)
|
||||||
implementation(projects.libraries.oidc.api)
|
implementation(projects.libraries.oidc.api)
|
||||||
implementation(projects.libraries.preferences.api)
|
implementation(projects.libraries.preferences.api)
|
||||||
|
|
@ -36,11 +38,13 @@ dependencies {
|
||||||
implementation(projects.libraries.pushproviders.api)
|
implementation(projects.libraries.pushproviders.api)
|
||||||
implementation(projects.libraries.designsystem)
|
implementation(projects.libraries.designsystem)
|
||||||
implementation(projects.libraries.matrixui)
|
implementation(projects.libraries.matrixui)
|
||||||
|
implementation(projects.libraries.uiCommon)
|
||||||
implementation(projects.libraries.uiStrings)
|
implementation(projects.libraries.uiStrings)
|
||||||
implementation(projects.features.login.api)
|
implementation(projects.features.login.api)
|
||||||
|
|
||||||
implementation(libs.coil)
|
implementation(libs.coil)
|
||||||
|
|
||||||
|
implementation(projects.features.announcement.api)
|
||||||
implementation(projects.features.ftue.api)
|
implementation(projects.features.ftue.api)
|
||||||
implementation(projects.features.share.api)
|
implementation(projects.features.share.api)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ import com.bumble.appyx.core.plugin.Plugin
|
||||||
import com.bumble.appyx.core.plugin.plugins
|
import com.bumble.appyx.core.plugin.plugins
|
||||||
import dev.zacsweers.metro.AppScope
|
import dev.zacsweers.metro.AppScope
|
||||||
import dev.zacsweers.metro.Assisted
|
import dev.zacsweers.metro.Assisted
|
||||||
import dev.zacsweers.metro.Inject
|
import dev.zacsweers.metro.AssistedInject
|
||||||
import io.element.android.annotations.ContributesNode
|
import io.element.android.annotations.ContributesNode
|
||||||
import io.element.android.appnav.di.SessionGraphFactory
|
import io.element.android.appnav.di.SessionGraphFactory
|
||||||
import io.element.android.libraries.architecture.NodeInputs
|
import io.element.android.libraries.architecture.NodeInputs
|
||||||
|
|
@ -41,7 +41,7 @@ import kotlinx.parcelize.Parcelize
|
||||||
* This allow to inject objects with SessionScope in the constructor of [LoggedInFlowNode].
|
* This allow to inject objects with SessionScope in the constructor of [LoggedInFlowNode].
|
||||||
*/
|
*/
|
||||||
@ContributesNode(AppScope::class)
|
@ContributesNode(AppScope::class)
|
||||||
@Inject
|
@AssistedInject
|
||||||
class LoggedInAppScopeFlowNode(
|
class LoggedInAppScopeFlowNode(
|
||||||
@Assisted buildContext: BuildContext,
|
@Assisted buildContext: BuildContext,
|
||||||
@Assisted plugins: List<Plugin>,
|
@Assisted plugins: List<Plugin>,
|
||||||
|
|
@ -57,6 +57,7 @@ class LoggedInAppScopeFlowNode(
|
||||||
), DependencyInjectionGraphOwner {
|
), DependencyInjectionGraphOwner {
|
||||||
interface Callback : Plugin {
|
interface Callback : Plugin {
|
||||||
fun onOpenBugReport()
|
fun onOpenBugReport()
|
||||||
|
fun onAddAccount()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
|
|
@ -83,6 +84,10 @@ class LoggedInAppScopeFlowNode(
|
||||||
override fun onOpenBugReport() {
|
override fun onOpenBugReport() {
|
||||||
plugins<Callback>().forEach { it.onOpenBugReport() }
|
plugins<Callback>().forEach { it.onOpenBugReport() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onAddAccount() {
|
||||||
|
plugins<Callback>().forEach { it.onAddAccount() }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return createNode<LoggedInFlowNode>(buildContext, listOf(callback))
|
return createNode<LoggedInFlowNode>(buildContext, listOf(callback))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -31,9 +31,17 @@ class LoggedInEventProcessor(
|
||||||
observingJob = roomMembershipObserver.updates
|
observingJob = roomMembershipObserver.updates
|
||||||
.filter { !it.isUserInRoom }
|
.filter { !it.isUserInRoom }
|
||||||
.distinctUntilChanged()
|
.distinctUntilChanged()
|
||||||
.onEach {
|
.onEach { roomMemberShipUpdate ->
|
||||||
when (it.change) {
|
when (roomMemberShipUpdate.change) {
|
||||||
MembershipChange.LEFT -> displayMessage(CommonStrings.common_current_user_left_room)
|
MembershipChange.LEFT -> {
|
||||||
|
displayMessage(
|
||||||
|
if (roomMemberShipUpdate.isSpace) {
|
||||||
|
CommonStrings.common_current_user_left_space
|
||||||
|
} else {
|
||||||
|
CommonStrings.common_current_user_left_room
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
MembershipChange.INVITATION_REJECTED -> displayMessage(CommonStrings.common_current_user_rejected_invite)
|
MembershipChange.INVITATION_REJECTED -> displayMessage(CommonStrings.common_current_user_rejected_invite)
|
||||||
MembershipChange.KNOCK_RETRACTED -> displayMessage(CommonStrings.common_current_user_canceled_knock)
|
MembershipChange.KNOCK_RETRACTED -> displayMessage(CommonStrings.common_current_user_canceled_knock)
|
||||||
else -> Unit
|
else -> Unit
|
||||||
|
|
|
||||||
|
|
@ -36,9 +36,8 @@ import com.bumble.appyx.navmodel.backstack.operation.pop
|
||||||
import com.bumble.appyx.navmodel.backstack.operation.push
|
import com.bumble.appyx.navmodel.backstack.operation.push
|
||||||
import com.bumble.appyx.navmodel.backstack.operation.replace
|
import com.bumble.appyx.navmodel.backstack.operation.replace
|
||||||
import com.bumble.appyx.navmodel.backstack.operation.singleTop
|
import com.bumble.appyx.navmodel.backstack.operation.singleTop
|
||||||
import dev.zacsweers.metro.AppScope
|
|
||||||
import dev.zacsweers.metro.Assisted
|
import dev.zacsweers.metro.Assisted
|
||||||
import dev.zacsweers.metro.Inject
|
import dev.zacsweers.metro.AssistedInject
|
||||||
import im.vector.app.features.analytics.plan.JoinedRoom
|
import im.vector.app.features.analytics.plan.JoinedRoom
|
||||||
import io.element.android.annotations.ContributesNode
|
import io.element.android.annotations.ContributesNode
|
||||||
import io.element.android.appnav.loggedin.LoggedInNode
|
import io.element.android.appnav.loggedin.LoggedInNode
|
||||||
|
|
@ -75,13 +74,13 @@ import io.element.android.libraries.matrix.api.core.EventId
|
||||||
import io.element.android.libraries.matrix.api.core.MAIN_SPACE
|
import io.element.android.libraries.matrix.api.core.MAIN_SPACE
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
|
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
|
||||||
import io.element.android.libraries.matrix.api.core.SessionId
|
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
|
import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
|
||||||
import io.element.android.libraries.matrix.api.permalink.PermalinkData
|
import io.element.android.libraries.matrix.api.permalink.PermalinkData
|
||||||
import io.element.android.libraries.matrix.api.verification.SessionVerificationServiceListener
|
import io.element.android.libraries.matrix.api.verification.SessionVerificationServiceListener
|
||||||
import io.element.android.libraries.matrix.api.verification.VerificationRequest
|
import io.element.android.libraries.matrix.api.verification.VerificationRequest
|
||||||
import io.element.android.libraries.push.api.notifications.conversations.NotificationConversationService
|
import io.element.android.libraries.push.api.notifications.conversations.NotificationConversationService
|
||||||
|
import io.element.android.libraries.ui.common.nodes.emptyNode
|
||||||
import io.element.android.services.appnavstate.api.AppNavigationStateService
|
import io.element.android.services.appnavstate.api.AppNavigationStateService
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
|
|
@ -100,7 +99,7 @@ import kotlin.time.Duration.Companion.seconds
|
||||||
import kotlin.time.toKotlinDuration
|
import kotlin.time.toKotlinDuration
|
||||||
|
|
||||||
@ContributesNode(SessionScope::class)
|
@ContributesNode(SessionScope::class)
|
||||||
@Inject
|
@AssistedInject
|
||||||
class LoggedInFlowNode(
|
class LoggedInFlowNode(
|
||||||
@Assisted buildContext: BuildContext,
|
@Assisted buildContext: BuildContext,
|
||||||
@Assisted plugins: List<Plugin>,
|
@Assisted plugins: List<Plugin>,
|
||||||
|
|
@ -138,11 +137,12 @@ class LoggedInFlowNode(
|
||||||
) {
|
) {
|
||||||
interface Callback : Plugin {
|
interface Callback : Plugin {
|
||||||
fun onOpenBugReport()
|
fun onOpenBugReport()
|
||||||
|
fun onAddAccount()
|
||||||
}
|
}
|
||||||
|
|
||||||
private val loggedInFlowProcessor = LoggedInEventProcessor(
|
private val loggedInFlowProcessor = LoggedInEventProcessor(
|
||||||
snackbarDispatcher,
|
snackbarDispatcher = snackbarDispatcher,
|
||||||
matrixClient.roomMembershipObserver(),
|
roomMembershipObserver = matrixClient.roomMembershipObserver,
|
||||||
)
|
)
|
||||||
|
|
||||||
private val verificationListener = object : SessionVerificationServiceListener {
|
private val verificationListener = object : SessionVerificationServiceListener {
|
||||||
|
|
@ -189,7 +189,7 @@ class LoggedInFlowNode(
|
||||||
// TODO We do not support Space yet, so directly navigate to main space
|
// TODO We do not support Space yet, so directly navigate to main space
|
||||||
appNavigationStateService.onNavigateToSpace(id, MAIN_SPACE)
|
appNavigationStateService.onNavigateToSpace(id, MAIN_SPACE)
|
||||||
loggedInFlowProcessor.observeEvents(sessionCoroutineScope)
|
loggedInFlowProcessor.observeEvents(sessionCoroutineScope)
|
||||||
matrixClient.sessionVerificationService().setListener(verificationListener)
|
matrixClient.sessionVerificationService.setListener(verificationListener)
|
||||||
mediaPreviewConfigMigration()
|
mediaPreviewConfigMigration()
|
||||||
|
|
||||||
sessionCoroutineScope.launch {
|
sessionCoroutineScope.launch {
|
||||||
|
|
@ -218,7 +218,7 @@ class LoggedInFlowNode(
|
||||||
appNavigationStateService.onLeavingSpace(id)
|
appNavigationStateService.onLeavingSpace(id)
|
||||||
appNavigationStateService.onLeavingSession(id)
|
appNavigationStateService.onLeavingSession(id)
|
||||||
loggedInFlowProcessor.stopObserving()
|
loggedInFlowProcessor.stopObserving()
|
||||||
matrixClient.sessionVerificationService().setListener(null)
|
matrixClient.sessionVerificationService.setListener(null)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
setupSendingQueue()
|
setupSendingQueue()
|
||||||
|
|
@ -281,7 +281,7 @@ class LoggedInFlowNode(
|
||||||
|
|
||||||
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||||
return when (navTarget) {
|
return when (navTarget) {
|
||||||
NavTarget.Placeholder -> createNode<PlaceholderNode>(buildContext)
|
NavTarget.Placeholder -> emptyNode(buildContext)
|
||||||
NavTarget.LoggedInPermanent -> {
|
NavTarget.LoggedInPermanent -> {
|
||||||
val callback = object : LoggedInNode.Callback {
|
val callback = object : LoggedInNode.Callback {
|
||||||
override fun navigateToNotificationTroubleshoot() {
|
override fun navigateToNotificationTroubleshoot() {
|
||||||
|
|
@ -366,8 +366,8 @@ class LoggedInFlowNode(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val spaceCallback = object : SpaceEntryPoint.Callback {
|
val spaceCallback = object : SpaceEntryPoint.Callback {
|
||||||
override fun onOpenRoom(roomId: RoomId) {
|
override fun onOpenRoom(roomId: RoomId, viaParameters: List<String>) {
|
||||||
backstack.push(NavTarget.Room(roomId.toRoomIdOrAlias()))
|
backstack.push(NavTarget.Room(roomId.toRoomIdOrAlias(), serverNames = viaParameters))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val inputs = RoomFlowNode.Inputs(
|
val inputs = RoomFlowNode.Inputs(
|
||||||
|
|
@ -392,6 +392,10 @@ class LoggedInFlowNode(
|
||||||
}
|
}
|
||||||
is NavTarget.Settings -> {
|
is NavTarget.Settings -> {
|
||||||
val callback = object : PreferencesEntryPoint.Callback {
|
val callback = object : PreferencesEntryPoint.Callback {
|
||||||
|
override fun onAddAccount() {
|
||||||
|
plugins<Callback>().forEach { it.onAddAccount() }
|
||||||
|
}
|
||||||
|
|
||||||
override fun onOpenBugReport() {
|
override fun onOpenBugReport() {
|
||||||
plugins<Callback>().forEach { it.onOpenBugReport() }
|
plugins<Callback>().forEach { it.onOpenBugReport() }
|
||||||
}
|
}
|
||||||
|
|
@ -404,11 +408,7 @@ class LoggedInFlowNode(
|
||||||
backstack.push(NavTarget.Room(roomId.toRoomIdOrAlias(), initialElement = RoomNavigationTarget.NotificationSettings))
|
backstack.push(NavTarget.Room(roomId.toRoomIdOrAlias(), initialElement = RoomNavigationTarget.NotificationSettings))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun navigateTo(sessionId: SessionId, roomId: RoomId, eventId: EventId) {
|
override fun navigateTo(roomId: RoomId, eventId: EventId) {
|
||||||
// We do not check the sessionId, but it will have to be done at some point (multi account)
|
|
||||||
if (sessionId != matrixClient.sessionId) {
|
|
||||||
Timber.e("SessionId mismatch, expected ${matrixClient.sessionId} but got $sessionId")
|
|
||||||
}
|
|
||||||
backstack.push(NavTarget.Room(roomId.toRoomIdOrAlias(), initialElement = RoomNavigationTarget.Messages(eventId)))
|
backstack.push(NavTarget.Room(roomId.toRoomIdOrAlias(), initialElement = RoomNavigationTarget.Messages(eventId)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -548,13 +548,6 @@ class LoggedInFlowNode(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ContributesNode(AppScope::class)
|
|
||||||
@Inject
|
|
||||||
class PlaceholderNode(
|
|
||||||
@Assisted buildContext: BuildContext,
|
|
||||||
@Assisted plugins: List<Plugin>,
|
|
||||||
) : Node(buildContext, plugins = plugins)
|
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
private class AttachRoomOperation(
|
private class AttachRoomOperation(
|
||||||
val roomTarget: LoggedInFlowNode.NavTarget.Room,
|
val roomTarget: LoggedInFlowNode.NavTarget.Room,
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ import com.bumble.appyx.core.plugin.plugins
|
||||||
import com.bumble.appyx.navmodel.backstack.BackStack
|
import com.bumble.appyx.navmodel.backstack.BackStack
|
||||||
import dev.zacsweers.metro.AppScope
|
import dev.zacsweers.metro.AppScope
|
||||||
import dev.zacsweers.metro.Assisted
|
import dev.zacsweers.metro.Assisted
|
||||||
import dev.zacsweers.metro.Inject
|
import dev.zacsweers.metro.AssistedInject
|
||||||
import io.element.android.annotations.ContributesNode
|
import io.element.android.annotations.ContributesNode
|
||||||
import io.element.android.features.login.api.LoginEntryPoint
|
import io.element.android.features.login.api.LoginEntryPoint
|
||||||
import io.element.android.features.login.api.LoginParams
|
import io.element.android.features.login.api.LoginParams
|
||||||
|
|
@ -36,7 +36,7 @@ import io.element.android.libraries.matrix.ui.media.NotLoggedInImageLoaderFactor
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
@ContributesNode(AppScope::class)
|
@ContributesNode(AppScope::class)
|
||||||
@Inject
|
@AssistedInject
|
||||||
class NotLoggedInFlowNode(
|
class NotLoggedInFlowNode(
|
||||||
@Assisted buildContext: BuildContext,
|
@Assisted buildContext: BuildContext,
|
||||||
@Assisted plugins: List<Plugin>,
|
@Assisted plugins: List<Plugin>,
|
||||||
|
|
|
||||||
|
|
@ -9,23 +9,23 @@ package io.element.android.appnav
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.animation.core.Spring
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.animation.core.spring
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.bumble.appyx.core.modality.BuildContext
|
import com.bumble.appyx.core.modality.BuildContext
|
||||||
import com.bumble.appyx.core.node.Node
|
import com.bumble.appyx.core.node.Node
|
||||||
import com.bumble.appyx.core.node.node
|
|
||||||
import com.bumble.appyx.core.plugin.Plugin
|
import com.bumble.appyx.core.plugin.Plugin
|
||||||
import com.bumble.appyx.core.state.MutableSavedStateMap
|
import com.bumble.appyx.core.state.MutableSavedStateMap
|
||||||
import com.bumble.appyx.navmodel.backstack.BackStack
|
import com.bumble.appyx.navmodel.backstack.BackStack
|
||||||
import com.bumble.appyx.navmodel.backstack.operation.pop
|
import com.bumble.appyx.navmodel.backstack.operation.pop
|
||||||
import com.bumble.appyx.navmodel.backstack.operation.push
|
import com.bumble.appyx.navmodel.backstack.operation.push
|
||||||
|
import com.bumble.appyx.navmodel.backstack.transitionhandler.rememberBackstackFader
|
||||||
|
import com.bumble.appyx.navmodel.backstack.transitionhandler.rememberBackstackSlider
|
||||||
import dev.zacsweers.metro.AppScope
|
import dev.zacsweers.metro.AppScope
|
||||||
import dev.zacsweers.metro.Assisted
|
import dev.zacsweers.metro.Assisted
|
||||||
import dev.zacsweers.metro.Inject
|
import dev.zacsweers.metro.AssistedInject
|
||||||
import im.vector.app.features.analytics.plan.JoinedRoom
|
import im.vector.app.features.analytics.plan.JoinedRoom
|
||||||
import io.element.android.annotations.ContributesNode
|
import io.element.android.annotations.ContributesNode
|
||||||
import io.element.android.appnav.di.MatrixSessionCache
|
import io.element.android.appnav.di.MatrixSessionCache
|
||||||
|
|
@ -34,18 +34,22 @@ import io.element.android.appnav.intent.ResolvedIntent
|
||||||
import io.element.android.appnav.root.RootNavStateFlowFactory
|
import io.element.android.appnav.root.RootNavStateFlowFactory
|
||||||
import io.element.android.appnav.root.RootPresenter
|
import io.element.android.appnav.root.RootPresenter
|
||||||
import io.element.android.appnav.root.RootView
|
import io.element.android.appnav.root.RootView
|
||||||
|
import io.element.android.features.announcement.api.AnnouncementService
|
||||||
import io.element.android.features.login.api.LoginParams
|
import io.element.android.features.login.api.LoginParams
|
||||||
import io.element.android.features.login.api.accesscontrol.AccountProviderAccessControl
|
import io.element.android.features.login.api.accesscontrol.AccountProviderAccessControl
|
||||||
import io.element.android.features.rageshake.api.bugreport.BugReportEntryPoint
|
import io.element.android.features.rageshake.api.bugreport.BugReportEntryPoint
|
||||||
import io.element.android.features.rageshake.api.reporter.BugReporter
|
import io.element.android.features.rageshake.api.reporter.BugReporter
|
||||||
import io.element.android.features.signedout.api.SignedOutEntryPoint
|
import io.element.android.features.signedout.api.SignedOutEntryPoint
|
||||||
|
import io.element.android.libraries.accountselect.api.AccountSelectEntryPoint
|
||||||
import io.element.android.libraries.architecture.BackstackView
|
import io.element.android.libraries.architecture.BackstackView
|
||||||
import io.element.android.libraries.architecture.BaseFlowNode
|
import io.element.android.libraries.architecture.BaseFlowNode
|
||||||
|
import io.element.android.libraries.architecture.appyx.rememberDelegateTransitionHandler
|
||||||
import io.element.android.libraries.architecture.createNode
|
import io.element.android.libraries.architecture.createNode
|
||||||
import io.element.android.libraries.architecture.waitForChildAttached
|
import io.element.android.libraries.architecture.waitForChildAttached
|
||||||
import io.element.android.libraries.core.uri.ensureProtocol
|
import io.element.android.libraries.core.uri.ensureProtocol
|
||||||
import io.element.android.libraries.deeplink.api.DeeplinkData
|
import io.element.android.libraries.deeplink.api.DeeplinkData
|
||||||
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
|
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||||
|
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||||
import io.element.android.libraries.matrix.api.core.SessionId
|
import io.element.android.libraries.matrix.api.core.SessionId
|
||||||
import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
|
import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
|
||||||
import io.element.android.libraries.matrix.api.permalink.PermalinkData
|
import io.element.android.libraries.matrix.api.permalink.PermalinkData
|
||||||
|
|
@ -53,14 +57,16 @@ import io.element.android.libraries.oidc.api.OidcAction
|
||||||
import io.element.android.libraries.oidc.api.OidcActionFlow
|
import io.element.android.libraries.oidc.api.OidcActionFlow
|
||||||
import io.element.android.libraries.sessionstorage.api.LoggedInState
|
import io.element.android.libraries.sessionstorage.api.LoggedInState
|
||||||
import io.element.android.libraries.sessionstorage.api.SessionStore
|
import io.element.android.libraries.sessionstorage.api.SessionStore
|
||||||
|
import io.element.android.libraries.ui.common.nodes.emptyNode
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
@ContributesNode(AppScope::class)
|
@ContributesNode(AppScope::class)
|
||||||
@Inject
|
@AssistedInject
|
||||||
class RootFlowNode(
|
class RootFlowNode(
|
||||||
@Assisted val buildContext: BuildContext,
|
@Assisted val buildContext: BuildContext,
|
||||||
@Assisted plugins: List<Plugin>,
|
@Assisted plugins: List<Plugin>,
|
||||||
|
|
@ -71,9 +77,12 @@ class RootFlowNode(
|
||||||
private val presenter: RootPresenter,
|
private val presenter: RootPresenter,
|
||||||
private val bugReportEntryPoint: BugReportEntryPoint,
|
private val bugReportEntryPoint: BugReportEntryPoint,
|
||||||
private val signedOutEntryPoint: SignedOutEntryPoint,
|
private val signedOutEntryPoint: SignedOutEntryPoint,
|
||||||
|
private val accountSelectEntryPoint: AccountSelectEntryPoint,
|
||||||
private val intentResolver: IntentResolver,
|
private val intentResolver: IntentResolver,
|
||||||
private val oidcActionFlow: OidcActionFlow,
|
private val oidcActionFlow: OidcActionFlow,
|
||||||
private val bugReporter: BugReporter,
|
private val bugReporter: BugReporter,
|
||||||
|
private val featureFlagService: FeatureFlagService,
|
||||||
|
private val announcementService: AnnouncementService,
|
||||||
) : BaseFlowNode<RootFlowNode.NavTarget>(
|
) : BaseFlowNode<RootFlowNode.NavTarget>(
|
||||||
backstack = BackStack(
|
backstack = BackStack(
|
||||||
initialElement = NavTarget.SplashScreen,
|
initialElement = NavTarget.SplashScreen,
|
||||||
|
|
@ -95,27 +104,24 @@ class RootFlowNode(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun observeNavState() {
|
private fun observeNavState() {
|
||||||
navStateFlowFactory.create(buildContext.savedStateMap)
|
navStateFlowFactory.create(buildContext.savedStateMap).distinctUntilChanged().onEach { navState ->
|
||||||
.distinctUntilChanged()
|
Timber.v("navState=$navState")
|
||||||
.onEach { navState ->
|
when (navState.loggedInState) {
|
||||||
Timber.v("navState=$navState")
|
is LoggedInState.LoggedIn -> {
|
||||||
when (navState.loggedInState) {
|
if (navState.loggedInState.isTokenValid) {
|
||||||
is LoggedInState.LoggedIn -> {
|
tryToRestoreLatestSession(
|
||||||
if (navState.loggedInState.isTokenValid) {
|
onSuccess = { sessionId -> switchToLoggedInFlow(sessionId, navState.cacheIndex) },
|
||||||
tryToRestoreLatestSession(
|
onFailure = { switchToNotLoggedInFlow(null) }
|
||||||
onSuccess = { sessionId -> switchToLoggedInFlow(sessionId, navState.cacheIndex) },
|
)
|
||||||
onFailure = { switchToNotLoggedInFlow(null) }
|
} else {
|
||||||
)
|
switchToSignedOutFlow(SessionId(navState.loggedInState.sessionId))
|
||||||
} else {
|
|
||||||
switchToSignedOutFlow(SessionId(navState.loggedInState.sessionId))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
LoggedInState.NotLoggedIn -> {
|
|
||||||
switchToNotLoggedInFlow(null)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
LoggedInState.NotLoggedIn -> {
|
||||||
|
switchToNotLoggedInFlow(null)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.launchIn(lifecycleScope)
|
}.launchIn(lifecycleScope)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun switchToLoggedInFlow(sessionId: SessionId, navId: Int) {
|
private fun switchToLoggedInFlow(sessionId: SessionId, navId: Int) {
|
||||||
|
|
@ -137,20 +143,17 @@ class RootFlowNode(
|
||||||
onFailure: () -> Unit,
|
onFailure: () -> Unit,
|
||||||
onSuccess: (SessionId) -> Unit,
|
onSuccess: (SessionId) -> Unit,
|
||||||
) {
|
) {
|
||||||
matrixSessionCache.getOrRestore(sessionId)
|
matrixSessionCache.getOrRestore(sessionId).onSuccess {
|
||||||
.onSuccess {
|
Timber.v("Succeed to restore session $sessionId")
|
||||||
Timber.v("Succeed to restore session $sessionId")
|
onSuccess(sessionId)
|
||||||
onSuccess(sessionId)
|
}.onFailure {
|
||||||
}
|
Timber.e(it, "Failed to restore session $sessionId")
|
||||||
.onFailure {
|
onFailure()
|
||||||
Timber.e(it, "Failed to restore session $sessionId")
|
}
|
||||||
onFailure()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun tryToRestoreLatestSession(
|
private suspend fun tryToRestoreLatestSession(
|
||||||
onSuccess: (SessionId) -> Unit,
|
onSuccess: (SessionId) -> Unit, onFailure: () -> Unit
|
||||||
onFailure: () -> Unit
|
|
||||||
) {
|
) {
|
||||||
val latestSessionId = sessionStore.getLatestSessionId()
|
val latestSessionId = sessionStore.getLatestSessionId()
|
||||||
if (latestSessionId == null) {
|
if (latestSessionId == null) {
|
||||||
|
|
@ -172,45 +175,64 @@ class RootFlowNode(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
onOpenBugReport = this::onOpenBugReport,
|
onOpenBugReport = this::onOpenBugReport,
|
||||||
) {
|
) {
|
||||||
BackstackView()
|
val backstackSlider = rememberBackstackSlider<NavTarget>(
|
||||||
|
transitionSpec = { spring(stiffness = Spring.StiffnessMediumLow) },
|
||||||
|
)
|
||||||
|
val backstackFader = rememberBackstackFader<NavTarget>(
|
||||||
|
transitionSpec = { spring(stiffness = Spring.StiffnessMediumLow) },
|
||||||
|
)
|
||||||
|
val transitionHandler = rememberDelegateTransitionHandler<NavTarget, BackStack.State> { navTarget ->
|
||||||
|
when (navTarget) {
|
||||||
|
is NavTarget.SplashScreen,
|
||||||
|
is NavTarget.LoggedInFlow -> backstackFader
|
||||||
|
else -> backstackSlider
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BackstackView(transitionHandler = transitionHandler)
|
||||||
|
announcementService.Render(Modifier)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed interface NavTarget : Parcelable {
|
sealed interface NavTarget : Parcelable {
|
||||||
@Parcelize
|
@Parcelize data object SplashScreen : NavTarget
|
||||||
data object SplashScreen : NavTarget
|
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize data class AccountSelect(
|
||||||
data class NotLoggedInFlow(
|
val currentSessionId: SessionId,
|
||||||
|
val intent: Intent?,
|
||||||
|
val permalinkData: PermalinkData?,
|
||||||
|
) : NavTarget
|
||||||
|
|
||||||
|
@Parcelize data class NotLoggedInFlow(
|
||||||
val params: LoginParams?
|
val params: LoginParams?
|
||||||
) : NavTarget
|
) : NavTarget
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize data class LoggedInFlow(
|
||||||
data class LoggedInFlow(
|
val sessionId: SessionId, val navId: Int
|
||||||
val sessionId: SessionId,
|
|
||||||
val navId: Int
|
|
||||||
) : NavTarget
|
) : NavTarget
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize data class SignedOutFlow(
|
||||||
data class SignedOutFlow(
|
|
||||||
val sessionId: SessionId
|
val sessionId: SessionId
|
||||||
) : NavTarget
|
) : NavTarget
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize data object BugReport : NavTarget
|
||||||
data object BugReport : NavTarget
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||||
return when (navTarget) {
|
return when (navTarget) {
|
||||||
is NavTarget.LoggedInFlow -> {
|
is NavTarget.LoggedInFlow -> {
|
||||||
val matrixClient = matrixSessionCache.getOrNull(navTarget.sessionId) ?: return splashNode(buildContext).also {
|
val matrixClient = matrixSessionCache.getOrNull(navTarget.sessionId)
|
||||||
Timber.w("Couldn't find any session, go through SplashScreen")
|
?: return emptyNode(buildContext).also {
|
||||||
}
|
Timber.w("Couldn't find any session, go through SplashScreen")
|
||||||
|
}
|
||||||
val inputs = LoggedInAppScopeFlowNode.Inputs(matrixClient)
|
val inputs = LoggedInAppScopeFlowNode.Inputs(matrixClient)
|
||||||
val callback = object : LoggedInAppScopeFlowNode.Callback {
|
val callback = object : LoggedInAppScopeFlowNode.Callback {
|
||||||
override fun onOpenBugReport() {
|
override fun onOpenBugReport() {
|
||||||
backstack.push(NavTarget.BugReport)
|
backstack.push(NavTarget.BugReport)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onAddAccount() {
|
||||||
|
backstack.push(NavTarget.NotLoggedInFlow(null))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
createNode<LoggedInAppScopeFlowNode>(buildContext, plugins = listOf(inputs, callback))
|
createNode<LoggedInAppScopeFlowNode>(buildContext, plugins = listOf(inputs, callback))
|
||||||
}
|
}
|
||||||
|
|
@ -226,32 +248,46 @@ class RootFlowNode(
|
||||||
createNode<NotLoggedInFlowNode>(buildContext, plugins = listOf(params, callback))
|
createNode<NotLoggedInFlowNode>(buildContext, plugins = listOf(params, callback))
|
||||||
}
|
}
|
||||||
is NavTarget.SignedOutFlow -> {
|
is NavTarget.SignedOutFlow -> {
|
||||||
signedOutEntryPoint.nodeBuilder(this, buildContext)
|
signedOutEntryPoint.nodeBuilder(this, buildContext).params(
|
||||||
.params(
|
SignedOutEntryPoint.Params(
|
||||||
SignedOutEntryPoint.Params(
|
sessionId = navTarget.sessionId
|
||||||
sessionId = navTarget.sessionId
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
.build()
|
).build()
|
||||||
}
|
}
|
||||||
NavTarget.SplashScreen -> splashNode(buildContext)
|
NavTarget.SplashScreen -> emptyNode(buildContext)
|
||||||
NavTarget.BugReport -> {
|
NavTarget.BugReport -> {
|
||||||
val callback = object : BugReportEntryPoint.Callback {
|
val callback = object : BugReportEntryPoint.Callback {
|
||||||
override fun onDone() {
|
override fun onDone() {
|
||||||
backstack.pop()
|
backstack.pop()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
bugReportEntryPoint
|
bugReportEntryPoint.nodeBuilder(this, buildContext).callback(callback).build()
|
||||||
.nodeBuilder(this, buildContext)
|
|
||||||
.callback(callback)
|
|
||||||
.build()
|
|
||||||
}
|
}
|
||||||
}
|
is NavTarget.AccountSelect -> {
|
||||||
}
|
val callback: AccountSelectEntryPoint.Callback = object : AccountSelectEntryPoint.Callback {
|
||||||
|
override fun onSelectAccount(sessionId: SessionId) {
|
||||||
|
lifecycleScope.launch {
|
||||||
|
if (sessionId == navTarget.currentSessionId) {
|
||||||
|
// Ensure that the account selection Node is removed from the backstack
|
||||||
|
// Do not pop when the account is changed to avoid a UI flicker.
|
||||||
|
backstack.pop()
|
||||||
|
}
|
||||||
|
attachSession(sessionId).apply {
|
||||||
|
if (navTarget.intent != null) {
|
||||||
|
attachIncomingShare(navTarget.intent)
|
||||||
|
} else if (navTarget.permalinkData != null) {
|
||||||
|
attachPermalinkData(navTarget.permalinkData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun splashNode(buildContext: BuildContext) = node(buildContext) {
|
override fun onCancel() {
|
||||||
Box(modifier = it.fillMaxSize(), contentAlignment = Alignment.Center) {
|
backstack.pop()
|
||||||
CircularProgressIndicator()
|
}
|
||||||
|
}
|
||||||
|
accountSelectEntryPoint.nodeBuilder(this, buildContext).callback(callback).build()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -267,19 +303,29 @@ class RootFlowNode(
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun onLoginLink(params: LoginParams) {
|
private suspend fun onLoginLink(params: LoginParams) {
|
||||||
// Is there a session already?
|
if (accountProviderAccessControl.isAllowedToConnectToAccountProvider(params.accountProvider.ensureProtocol())) {
|
||||||
val latestSessionId = sessionStore.getLatestSessionId()
|
// Is there a session already?
|
||||||
if (latestSessionId == null) {
|
val sessions = sessionStore.getAllSessions()
|
||||||
// No session, open login
|
if (sessions.isNotEmpty()) {
|
||||||
if (accountProviderAccessControl.isAllowedToConnectToAccountProvider(params.accountProvider.ensureProtocol())) {
|
if (featureFlagService.isFeatureEnabled(FeatureFlags.MultiAccount)) {
|
||||||
switchToNotLoggedInFlow(params)
|
val loginHintMatrixId = params.loginHint?.removePrefix("mxid:")
|
||||||
|
val existingAccount = sessions.find { it.userId == loginHintMatrixId }
|
||||||
|
if (existingAccount != null) {
|
||||||
|
// We have an existing account matching the login hint, ensure this is the current session
|
||||||
|
sessionStore.setLatestSession(existingAccount.userId)
|
||||||
|
} else {
|
||||||
|
val latestSessionId = sessions.maxBy { it.lastUsageIndex }.userId
|
||||||
|
attachSession(SessionId(latestSessionId))
|
||||||
|
backstack.push(NavTarget.NotLoggedInFlow(params))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Timber.w("Login link ignored, multi account is disabled")
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
Timber.w("Login link ignored, we are not allowed to connect to the homeserver")
|
switchToNotLoggedInFlow(params)
|
||||||
switchToNotLoggedInFlow(null)
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Just ignore the login link if we already have a session
|
Timber.w("Login link ignored, we are not allowed to connect to the homeserver")
|
||||||
Timber.w("Login link ignored, we already have a session")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -290,56 +336,95 @@ class RootFlowNode(
|
||||||
// No session, open login
|
// No session, open login
|
||||||
switchToNotLoggedInFlow(null)
|
switchToNotLoggedInFlow(null)
|
||||||
} else {
|
} else {
|
||||||
attachSession(latestSessionId)
|
// wait for the current session to be restored
|
||||||
.attachIncomingShare(intent)
|
val loggedInFlowNode = attachSession(latestSessionId)
|
||||||
|
if (sessionStore.getAllSessions().size > 1) {
|
||||||
|
// Several accounts, let the user choose which one to use
|
||||||
|
backstack.push(
|
||||||
|
NavTarget.AccountSelect(
|
||||||
|
currentSessionId = latestSessionId,
|
||||||
|
intent = intent,
|
||||||
|
permalinkData = null,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// Only one account, directly attach the incoming share node.
|
||||||
|
loggedInFlowNode.attachIncomingShare(intent)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun navigateTo(permalinkData: PermalinkData) {
|
private suspend fun navigateTo(permalinkData: PermalinkData) {
|
||||||
Timber.d("Navigating to $permalinkData")
|
Timber.d("Navigating to $permalinkData")
|
||||||
attachSession(null)
|
// Is there a session already?
|
||||||
.apply {
|
val latestSessionId = sessionStore.getLatestSessionId()
|
||||||
when (permalinkData) {
|
if (latestSessionId == null) {
|
||||||
is PermalinkData.FallbackLink -> Unit
|
// No session, open login
|
||||||
is PermalinkData.RoomEmailInviteLink -> Unit
|
switchToNotLoggedInFlow(null)
|
||||||
is PermalinkData.RoomLink -> {
|
} else {
|
||||||
attachRoom(
|
// wait for the current session to be restored
|
||||||
roomIdOrAlias = permalinkData.roomIdOrAlias,
|
val loggedInFlowNode = attachSession(latestSessionId)
|
||||||
trigger = JoinedRoom.Trigger.MobilePermalink,
|
when (permalinkData) {
|
||||||
serverNames = permalinkData.viaParameters,
|
is PermalinkData.FallbackLink -> Unit
|
||||||
eventId = permalinkData.eventId,
|
is PermalinkData.RoomEmailInviteLink -> Unit
|
||||||
clearBackstack = true
|
else -> {
|
||||||
|
if (sessionStore.getAllSessions().size > 1) {
|
||||||
|
// Several accounts, let the user choose which one to use
|
||||||
|
backstack.push(
|
||||||
|
NavTarget.AccountSelect(
|
||||||
|
currentSessionId = latestSessionId,
|
||||||
|
intent = null,
|
||||||
|
permalinkData = permalinkData,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
} else {
|
||||||
is PermalinkData.UserLink -> {
|
// Only one account, directly attach the room or the user node.
|
||||||
attachUser(permalinkData.userId)
|
loggedInFlowNode.attachPermalinkData(permalinkData)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun LoggedInFlowNode.attachPermalinkData(permalinkData: PermalinkData) {
|
||||||
|
when (permalinkData) {
|
||||||
|
is PermalinkData.FallbackLink -> Unit
|
||||||
|
is PermalinkData.RoomEmailInviteLink -> Unit
|
||||||
|
is PermalinkData.RoomLink -> {
|
||||||
|
attachRoom(
|
||||||
|
roomIdOrAlias = permalinkData.roomIdOrAlias,
|
||||||
|
trigger = JoinedRoom.Trigger.MobilePermalink,
|
||||||
|
serverNames = permalinkData.viaParameters,
|
||||||
|
eventId = permalinkData.eventId,
|
||||||
|
clearBackstack = true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is PermalinkData.UserLink -> {
|
||||||
|
attachUser(permalinkData.userId)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun navigateTo(deeplinkData: DeeplinkData) {
|
private suspend fun navigateTo(deeplinkData: DeeplinkData) {
|
||||||
Timber.d("Navigating to $deeplinkData")
|
Timber.d("Navigating to $deeplinkData")
|
||||||
attachSession(deeplinkData.sessionId)
|
attachSession(deeplinkData.sessionId).apply {
|
||||||
.apply {
|
when (deeplinkData) {
|
||||||
when (deeplinkData) {
|
is DeeplinkData.Root -> Unit // The room list will always be shown, observing FtueState
|
||||||
is DeeplinkData.Root -> Unit // The room list will always be shown, observing FtueState
|
is DeeplinkData.Room -> attachRoom(deeplinkData.roomId.toRoomIdOrAlias(), clearBackstack = true)
|
||||||
is DeeplinkData.Room -> attachRoom(deeplinkData.roomId.toRoomIdOrAlias(), clearBackstack = true)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onOidcAction(oidcAction: OidcAction) {
|
private fun onOidcAction(oidcAction: OidcAction) {
|
||||||
oidcActionFlow.post(oidcAction)
|
oidcActionFlow.post(oidcAction)
|
||||||
}
|
}
|
||||||
|
|
||||||
// [sessionId] will be null for permalink.
|
private suspend fun attachSession(sessionId: SessionId): LoggedInFlowNode {
|
||||||
private suspend fun attachSession(sessionId: SessionId?): LoggedInFlowNode {
|
// Ensure that the session is the latest one
|
||||||
// TODO handle multi-session
|
sessionStore.setLatestSession(sessionId.value)
|
||||||
return waitForChildAttached<LoggedInAppScopeFlowNode, NavTarget> { navTarget ->
|
return waitForChildAttached<LoggedInAppScopeFlowNode, NavTarget> { navTarget ->
|
||||||
navTarget is NavTarget.LoggedInFlow && (sessionId == null || navTarget.sessionId == sessionId)
|
navTarget is NavTarget.LoggedInFlow && navTarget.sessionId == sessionId
|
||||||
}
|
}.attachSession()
|
||||||
.attachSession()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -109,7 +109,10 @@ class MatrixSessionCache(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onNewMatrixClient(matrixClient: MatrixClient) {
|
private fun onNewMatrixClient(matrixClient: MatrixClient) {
|
||||||
val syncOrchestrator = syncOrchestratorFactory.create(matrixClient)
|
val syncOrchestrator = syncOrchestratorFactory.create(
|
||||||
|
syncService = matrixClient.syncService,
|
||||||
|
sessionCoroutineScope = matrixClient.sessionCoroutineScope,
|
||||||
|
)
|
||||||
sessionIdsToMatrixSession[matrixClient.sessionId] = InMemoryMatrixSession(
|
sessionIdsToMatrixSession[matrixClient.sessionId] = InMemoryMatrixSession(
|
||||||
matrixClient = matrixClient,
|
matrixClient = matrixClient,
|
||||||
syncOrchestrator = syncOrchestrator,
|
syncOrchestrator = syncOrchestrator,
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,6 @@ package io.element.android.appnav.di
|
||||||
|
|
||||||
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
||||||
|
|
||||||
fun interface RoomComponentFactory {
|
fun interface RoomGraphFactory {
|
||||||
fun create(room: JoinedRoom): Any
|
fun create(room: JoinedRoom): Any
|
||||||
}
|
}
|
||||||
|
|
@ -10,14 +10,15 @@ package io.element.android.appnav.di
|
||||||
import androidx.annotation.VisibleForTesting
|
import androidx.annotation.VisibleForTesting
|
||||||
import dev.zacsweers.metro.Assisted
|
import dev.zacsweers.metro.Assisted
|
||||||
import dev.zacsweers.metro.AssistedFactory
|
import dev.zacsweers.metro.AssistedFactory
|
||||||
import dev.zacsweers.metro.Inject
|
import dev.zacsweers.metro.AssistedInject
|
||||||
import io.element.android.features.networkmonitor.api.NetworkMonitor
|
import io.element.android.features.networkmonitor.api.NetworkMonitor
|
||||||
import io.element.android.features.networkmonitor.api.NetworkStatus
|
import io.element.android.features.networkmonitor.api.NetworkStatus
|
||||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||||
import io.element.android.libraries.core.coroutine.childScope
|
import io.element.android.libraries.core.coroutine.childScope
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
import io.element.android.libraries.matrix.api.sync.SyncService
|
||||||
import io.element.android.libraries.matrix.api.sync.SyncState
|
import io.element.android.libraries.matrix.api.sync.SyncState
|
||||||
import io.element.android.services.appnavstate.api.AppForegroundStateService
|
import io.element.android.services.appnavstate.api.AppForegroundStateService
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.FlowPreview
|
import kotlinx.coroutines.FlowPreview
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.debounce
|
import kotlinx.coroutines.flow.debounce
|
||||||
|
|
@ -30,23 +31,25 @@ import java.util.concurrent.atomic.AtomicBoolean
|
||||||
import kotlin.time.Duration.Companion.milliseconds
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
import kotlin.time.Duration.Companion.seconds
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
@Inject
|
@AssistedInject
|
||||||
class SyncOrchestrator(
|
class SyncOrchestrator(
|
||||||
@Assisted matrixClient: MatrixClient,
|
@Assisted private val syncService: SyncService,
|
||||||
|
@Assisted sessionCoroutineScope: CoroutineScope,
|
||||||
private val appForegroundStateService: AppForegroundStateService,
|
private val appForegroundStateService: AppForegroundStateService,
|
||||||
private val networkMonitor: NetworkMonitor,
|
private val networkMonitor: NetworkMonitor,
|
||||||
dispatchers: CoroutineDispatchers,
|
dispatchers: CoroutineDispatchers,
|
||||||
) {
|
) {
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
interface Factory {
|
interface Factory {
|
||||||
fun create(matrixClient: MatrixClient): SyncOrchestrator
|
fun create(
|
||||||
|
syncService: SyncService,
|
||||||
|
sessionCoroutineScope: CoroutineScope,
|
||||||
|
): SyncOrchestrator
|
||||||
}
|
}
|
||||||
|
|
||||||
private val syncService = matrixClient.syncService()
|
|
||||||
|
|
||||||
private val tag = "SyncOrchestrator"
|
private val tag = "SyncOrchestrator"
|
||||||
|
|
||||||
private val coroutineScope = matrixClient.sessionCoroutineScope.childScope(dispatchers.io, tag)
|
private val coroutineScope = sessionCoroutineScope.childScope(dispatchers.io, tag)
|
||||||
|
|
||||||
private val started = AtomicBoolean(false)
|
private val started = AtomicBoolean(false)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,12 +14,12 @@ import com.bumble.appyx.core.node.Node
|
||||||
import com.bumble.appyx.core.plugin.Plugin
|
import com.bumble.appyx.core.plugin.Plugin
|
||||||
import com.bumble.appyx.core.plugin.plugins
|
import com.bumble.appyx.core.plugin.plugins
|
||||||
import dev.zacsweers.metro.Assisted
|
import dev.zacsweers.metro.Assisted
|
||||||
import dev.zacsweers.metro.Inject
|
import dev.zacsweers.metro.AssistedInject
|
||||||
import io.element.android.annotations.ContributesNode
|
import io.element.android.annotations.ContributesNode
|
||||||
import io.element.android.libraries.di.SessionScope
|
import io.element.android.libraries.di.SessionScope
|
||||||
|
|
||||||
@ContributesNode(SessionScope::class)
|
@ContributesNode(SessionScope::class)
|
||||||
@Inject
|
@AssistedInject
|
||||||
class LoggedInNode(
|
class LoggedInNode(
|
||||||
@Assisted buildContext: BuildContext,
|
@Assisted buildContext: BuildContext,
|
||||||
@Assisted plugins: List<Plugin>,
|
@Assisted plugins: List<Plugin>,
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ import com.bumble.appyx.core.plugin.plugins
|
||||||
import com.bumble.appyx.navmodel.backstack.BackStack
|
import com.bumble.appyx.navmodel.backstack.BackStack
|
||||||
import com.bumble.appyx.navmodel.backstack.operation.newRoot
|
import com.bumble.appyx.navmodel.backstack.operation.newRoot
|
||||||
import dev.zacsweers.metro.Assisted
|
import dev.zacsweers.metro.Assisted
|
||||||
import dev.zacsweers.metro.Inject
|
import dev.zacsweers.metro.AssistedInject
|
||||||
import im.vector.app.features.analytics.plan.JoinedRoom
|
import im.vector.app.features.analytics.plan.JoinedRoom
|
||||||
import io.element.android.annotations.ContributesNode
|
import io.element.android.annotations.ContributesNode
|
||||||
import io.element.android.appnav.room.joined.JoinedRoomFlowNode
|
import io.element.android.appnav.room.joined.JoinedRoomFlowNode
|
||||||
|
|
@ -64,7 +64,7 @@ import java.util.Optional
|
||||||
import kotlin.jvm.optionals.getOrNull
|
import kotlin.jvm.optionals.getOrNull
|
||||||
|
|
||||||
@ContributesNode(SessionScope::class)
|
@ContributesNode(SessionScope::class)
|
||||||
@Inject
|
@AssistedInject
|
||||||
class RoomFlowNode(
|
class RoomFlowNode(
|
||||||
@Assisted val buildContext: BuildContext,
|
@Assisted val buildContext: BuildContext,
|
||||||
@Assisted plugins: List<Plugin>,
|
@Assisted plugins: List<Plugin>,
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ import com.bumble.appyx.core.plugin.plugins
|
||||||
import com.bumble.appyx.navmodel.backstack.BackStack
|
import com.bumble.appyx.navmodel.backstack.BackStack
|
||||||
import com.bumble.appyx.navmodel.backstack.operation.newRoot
|
import com.bumble.appyx.navmodel.backstack.operation.newRoot
|
||||||
import dev.zacsweers.metro.Assisted
|
import dev.zacsweers.metro.Assisted
|
||||||
import dev.zacsweers.metro.Inject
|
import dev.zacsweers.metro.AssistedInject
|
||||||
import io.element.android.annotations.ContributesNode
|
import io.element.android.annotations.ContributesNode
|
||||||
import io.element.android.appnav.room.RoomNavigationTarget
|
import io.element.android.appnav.room.RoomNavigationTarget
|
||||||
import io.element.android.libraries.architecture.BackstackView
|
import io.element.android.libraries.architecture.BackstackView
|
||||||
|
|
@ -45,7 +45,7 @@ import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
@ContributesNode(SessionScope::class)
|
@ContributesNode(SessionScope::class)
|
||||||
@Inject
|
@AssistedInject
|
||||||
class JoinedRoomFlowNode(
|
class JoinedRoomFlowNode(
|
||||||
@Assisted val buildContext: BuildContext,
|
@Assisted val buildContext: BuildContext,
|
||||||
@Assisted plugins: List<Plugin>,
|
@Assisted plugins: List<Plugin>,
|
||||||
|
|
|
||||||
|
|
@ -18,9 +18,9 @@ import com.bumble.appyx.core.plugin.Plugin
|
||||||
import com.bumble.appyx.navmodel.backstack.BackStack
|
import com.bumble.appyx.navmodel.backstack.BackStack
|
||||||
import com.bumble.appyx.navmodel.backstack.operation.push
|
import com.bumble.appyx.navmodel.backstack.operation.push
|
||||||
import dev.zacsweers.metro.Assisted
|
import dev.zacsweers.metro.Assisted
|
||||||
import dev.zacsweers.metro.Inject
|
import dev.zacsweers.metro.AssistedInject
|
||||||
import io.element.android.annotations.ContributesNode
|
import io.element.android.annotations.ContributesNode
|
||||||
import io.element.android.appnav.di.RoomComponentFactory
|
import io.element.android.appnav.di.RoomGraphFactory
|
||||||
import io.element.android.appnav.room.RoomNavigationTarget
|
import io.element.android.appnav.room.RoomNavigationTarget
|
||||||
import io.element.android.features.messages.api.MessagesEntryPoint
|
import io.element.android.features.messages.api.MessagesEntryPoint
|
||||||
import io.element.android.features.roomdetails.api.RoomDetailsEntryPoint
|
import io.element.android.features.roomdetails.api.RoomDetailsEntryPoint
|
||||||
|
|
@ -45,7 +45,7 @@ import kotlinx.parcelize.Parcelize
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
@ContributesNode(SessionScope::class)
|
@ContributesNode(SessionScope::class)
|
||||||
@Inject
|
@AssistedInject
|
||||||
class JoinedRoomLoadedFlowNode(
|
class JoinedRoomLoadedFlowNode(
|
||||||
@Assisted buildContext: BuildContext,
|
@Assisted buildContext: BuildContext,
|
||||||
@Assisted plugins: List<Plugin>,
|
@Assisted plugins: List<Plugin>,
|
||||||
|
|
@ -56,7 +56,7 @@ class JoinedRoomLoadedFlowNode(
|
||||||
private val sessionCoroutineScope: CoroutineScope,
|
private val sessionCoroutineScope: CoroutineScope,
|
||||||
private val matrixClient: MatrixClient,
|
private val matrixClient: MatrixClient,
|
||||||
private val activeRoomsHolder: ActiveRoomsHolder,
|
private val activeRoomsHolder: ActiveRoomsHolder,
|
||||||
roomComponentFactory: RoomComponentFactory,
|
roomGraphFactory: RoomGraphFactory,
|
||||||
) : BaseFlowNode<JoinedRoomLoadedFlowNode.NavTarget>(
|
) : BaseFlowNode<JoinedRoomLoadedFlowNode.NavTarget>(
|
||||||
backstack = BackStack(
|
backstack = BackStack(
|
||||||
initialElement = when (val input = plugins.filterIsInstance<Inputs>().first().initialElement) {
|
initialElement = when (val input = plugins.filterIsInstance<Inputs>().first().initialElement) {
|
||||||
|
|
@ -83,7 +83,7 @@ class JoinedRoomLoadedFlowNode(
|
||||||
|
|
||||||
private val inputs: Inputs = inputs()
|
private val inputs: Inputs = inputs()
|
||||||
private val callbacks = plugins.filterIsInstance<Callback>()
|
private val callbacks = plugins.filterIsInstance<Callback>()
|
||||||
override val graph = roomComponentFactory.create(inputs.room)
|
override val graph = roomGraphFactory.create(inputs.room)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
lifecycle.subscribe(
|
lifecycle.subscribe(
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ import com.bumble.appyx.navmodel.backstack.activeElement
|
||||||
import com.bumble.appyx.testing.junit4.util.MainDispatcherRule
|
import com.bumble.appyx.testing.junit4.util.MainDispatcherRule
|
||||||
import com.bumble.appyx.testing.unit.common.helper.parentNodeTestHelper
|
import com.bumble.appyx.testing.unit.common.helper.parentNodeTestHelper
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.appnav.di.RoomComponentFactory
|
import io.element.android.appnav.di.RoomGraphFactory
|
||||||
import io.element.android.appnav.room.RoomNavigationTarget
|
import io.element.android.appnav.room.RoomNavigationTarget
|
||||||
import io.element.android.appnav.room.joined.JoinedRoomLoadedFlowNode
|
import io.element.android.appnav.room.joined.JoinedRoomLoadedFlowNode
|
||||||
import io.element.android.features.messages.api.MessagesEntryPoint
|
import io.element.android.features.messages.api.MessagesEntryPoint
|
||||||
|
|
@ -70,7 +70,7 @@ class JoinedRoomLoadedFlowNodeTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class FakeRoomComponentFactory : RoomComponentFactory {
|
private class FakeRoomGraphFactory : RoomGraphFactory {
|
||||||
override fun create(room: JoinedRoom): Any {
|
override fun create(room: JoinedRoom): Any {
|
||||||
return Unit
|
return Unit
|
||||||
}
|
}
|
||||||
|
|
@ -110,7 +110,7 @@ class JoinedRoomLoadedFlowNodeTest {
|
||||||
roomDetailsEntryPoint = roomDetailsEntryPoint,
|
roomDetailsEntryPoint = roomDetailsEntryPoint,
|
||||||
appNavigationStateService = FakeAppNavigationStateService(),
|
appNavigationStateService = FakeAppNavigationStateService(),
|
||||||
sessionCoroutineScope = this,
|
sessionCoroutineScope = this,
|
||||||
roomComponentFactory = FakeRoomComponentFactory(),
|
roomGraphFactory = FakeRoomGraphFactory(),
|
||||||
matrixClient = FakeMatrixClient(),
|
matrixClient = FakeMatrixClient(),
|
||||||
activeRoomsHolder = activeRoomsHolder,
|
activeRoomsHolder = activeRoomsHolder,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@ import io.element.android.appnav.di.SyncOrchestrator
|
||||||
import io.element.android.features.networkmonitor.api.NetworkStatus
|
import io.element.android.features.networkmonitor.api.NetworkStatus
|
||||||
import io.element.android.features.networkmonitor.test.FakeNetworkMonitor
|
import io.element.android.features.networkmonitor.test.FakeNetworkMonitor
|
||||||
import io.element.android.libraries.matrix.api.sync.SyncState
|
import io.element.android.libraries.matrix.api.sync.SyncState
|
||||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
|
||||||
import io.element.android.libraries.matrix.test.sync.FakeSyncService
|
import io.element.android.libraries.matrix.test.sync.FakeSyncService
|
||||||
import io.element.android.services.appnavstate.test.FakeAppForegroundStateService
|
import io.element.android.services.appnavstate.test.FakeAppForegroundStateService
|
||||||
import io.element.android.tests.testutils.WarmUpRule
|
import io.element.android.tests.testutils.WarmUpRule
|
||||||
|
|
@ -385,7 +384,8 @@ class SyncOrchestratorTest {
|
||||||
networkMonitor: FakeNetworkMonitor = FakeNetworkMonitor(),
|
networkMonitor: FakeNetworkMonitor = FakeNetworkMonitor(),
|
||||||
appForegroundStateService: FakeAppForegroundStateService = FakeAppForegroundStateService(),
|
appForegroundStateService: FakeAppForegroundStateService = FakeAppForegroundStateService(),
|
||||||
) = SyncOrchestrator(
|
) = SyncOrchestrator(
|
||||||
matrixClient = FakeMatrixClient(syncService = syncService, sessionCoroutineScope = backgroundScope),
|
syncService = syncService,
|
||||||
|
sessionCoroutineScope = backgroundScope,
|
||||||
networkMonitor = networkMonitor,
|
networkMonitor = networkMonitor,
|
||||||
appForegroundStateService = appForegroundStateService,
|
appForegroundStateService = appForegroundStateService,
|
||||||
dispatchers = testCoroutineDispatchers(),
|
dispatchers = testCoroutineDispatchers(),
|
||||||
|
|
|
||||||
|
|
@ -10,12 +10,13 @@ package io.element.android.appnav.di
|
||||||
import com.bumble.appyx.core.state.MutableSavedStateMapImpl
|
import com.bumble.appyx.core.state.MutableSavedStateMapImpl
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.features.networkmonitor.test.FakeNetworkMonitor
|
import io.element.android.features.networkmonitor.test.FakeNetworkMonitor
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
import io.element.android.libraries.matrix.api.sync.SyncService
|
||||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||||
import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService
|
import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService
|
||||||
import io.element.android.services.appnavstate.test.FakeAppForegroundStateService
|
import io.element.android.services.appnavstate.test.FakeAppForegroundStateService
|
||||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.test.TestScope
|
import kotlinx.coroutines.test.TestScope
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
@ -117,9 +118,13 @@ class MatrixSessionCacheTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun TestScope.createSyncOrchestratorFactory() = object : SyncOrchestrator.Factory {
|
private fun TestScope.createSyncOrchestratorFactory() = object : SyncOrchestrator.Factory {
|
||||||
override fun create(matrixClient: MatrixClient): SyncOrchestrator {
|
override fun create(
|
||||||
|
syncService: SyncService,
|
||||||
|
sessionCoroutineScope: CoroutineScope,
|
||||||
|
): SyncOrchestrator {
|
||||||
return SyncOrchestrator(
|
return SyncOrchestrator(
|
||||||
matrixClient,
|
syncService = syncService,
|
||||||
|
sessionCoroutineScope = sessionCoroutineScope,
|
||||||
appForegroundStateService = FakeAppForegroundStateService(),
|
appForegroundStateService = FakeAppForegroundStateService(),
|
||||||
networkMonitor = FakeNetworkMonitor(),
|
networkMonitor = FakeNetworkMonitor(),
|
||||||
dispatchers = testCoroutineDispatchers(),
|
dispatchers = testCoroutineDispatchers(),
|
||||||
|
|
|
||||||
|
|
@ -111,7 +111,7 @@ class IntentResolverTest {
|
||||||
@Test
|
@Test
|
||||||
fun `test resolve oidc`() {
|
fun `test resolve oidc`() {
|
||||||
val sut = createIntentResolver(
|
val sut = createIntentResolver(
|
||||||
oidcIntentResolverResult = { OidcAction.GoBack },
|
oidcIntentResolverResult = { OidcAction.GoBack() },
|
||||||
)
|
)
|
||||||
val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply {
|
val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply {
|
||||||
action = Intent.ACTION_VIEW
|
action = Intent.ACTION_VIEW
|
||||||
|
|
@ -120,7 +120,7 @@ class IntentResolverTest {
|
||||||
val result = sut.resolve(intent)
|
val result = sut.resolve(intent)
|
||||||
assertThat(result).isEqualTo(
|
assertThat(result).isEqualTo(
|
||||||
ResolvedIntent.Oidc(
|
ResolvedIntent.Oidc(
|
||||||
oidcAction = OidcAction.GoBack
|
oidcAction = OidcAction.GoBack()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ plugins {
|
||||||
alias(libs.plugins.compose.compiler) apply false
|
alias(libs.plugins.compose.compiler) apply false
|
||||||
alias(libs.plugins.ksp) apply false
|
alias(libs.plugins.ksp) apply false
|
||||||
alias(libs.plugins.dependencycheck) apply false
|
alias(libs.plugins.dependencycheck) apply false
|
||||||
|
alias(libs.plugins.roborazzi) apply false
|
||||||
alias(libs.plugins.dependencyanalysis)
|
alias(libs.plugins.dependencyanalysis)
|
||||||
alias(libs.plugins.detekt)
|
alias(libs.plugins.detekt)
|
||||||
alias(libs.plugins.ktlint)
|
alias(libs.plugins.ktlint)
|
||||||
|
|
@ -192,6 +193,21 @@ subprojects {
|
||||||
tasks.findByName("recordPaparazziRelease")?.dependsOn(removeOldScreenshotsTask)
|
tasks.findByName("recordPaparazziRelease")?.dependsOn(removeOldScreenshotsTask)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Make sure to delete old snapshot before recording new ones
|
||||||
|
subprojects {
|
||||||
|
val screenshotsDir = File("${project.projectDir}/screenshots")
|
||||||
|
val removeOldScreenshotsTask = tasks.register("removeOldScreenshots") {
|
||||||
|
onlyIf { screenshotsDir.exists() }
|
||||||
|
doFirst {
|
||||||
|
println("Delete previous screenshots located at $screenshotsDir\n")
|
||||||
|
screenshotsDir.deleteRecursively()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tasks.findByName("recordRoborazzi")?.dependsOn(removeOldScreenshotsTask)
|
||||||
|
tasks.findByName("recordRoborazziDebug")?.dependsOn(removeOldScreenshotsTask)
|
||||||
|
tasks.findByName("recordRoborazziRelease")?.dependsOn(removeOldScreenshotsTask)
|
||||||
|
}
|
||||||
|
|
||||||
subprojects {
|
subprojects {
|
||||||
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
|
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
|
||||||
compilerOptions {
|
compilerOptions {
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@ import dev.zacsweers.metro.BindingContainer
|
||||||
import dev.zacsweers.metro.Binds
|
import dev.zacsweers.metro.Binds
|
||||||
import dev.zacsweers.metro.ContributesTo
|
import dev.zacsweers.metro.ContributesTo
|
||||||
import dev.zacsweers.metro.IntoMap
|
import dev.zacsweers.metro.IntoMap
|
||||||
|
import dev.zacsweers.metro.Origin
|
||||||
import io.element.android.annotations.ContributesNode
|
import io.element.android.annotations.ContributesNode
|
||||||
import org.jetbrains.kotlin.name.FqName
|
import org.jetbrains.kotlin.name.FqName
|
||||||
|
|
||||||
|
|
@ -71,14 +72,16 @@ class ContributesNodeProcessor(
|
||||||
val scope = annotation.arguments.find { it.name?.asString() == "scope" }!!.value as KSType
|
val scope = annotation.arguments.find { it.name?.asString() == "scope" }!!.value as KSType
|
||||||
val modulePackage = ksClass.packageName.asString()
|
val modulePackage = ksClass.packageName.asString()
|
||||||
val moduleClassName = "${ksClass.simpleName.asString()}_Module"
|
val moduleClassName = "${ksClass.simpleName.asString()}_Module"
|
||||||
|
val nodeClassName = ClassName.bestGuess(ksClass.qualifiedName!!.asString())
|
||||||
val content = FileSpec.builder(
|
val content = FileSpec.builder(
|
||||||
packageName = modulePackage,
|
packageName = modulePackage,
|
||||||
fileName = moduleClassName,
|
fileName = moduleClassName,
|
||||||
)
|
)
|
||||||
.addType(
|
.addType(
|
||||||
TypeSpec.interfaceBuilder(moduleClassName)
|
TypeSpec.interfaceBuilder(moduleClassName)
|
||||||
|
.addAnnotation(AnnotationSpec.builder(Origin::class).addMember(CLASS_PLACEHOLDER, nodeClassName).build())
|
||||||
.addAnnotation(BindingContainer::class)
|
.addAnnotation(BindingContainer::class)
|
||||||
.addAnnotation(AnnotationSpec.builder(ContributesTo::class).addMember("%T::class", scope.toTypeName()).build())
|
.addAnnotation(AnnotationSpec.builder(ContributesTo::class).addMember(CLASS_PLACEHOLDER, scope.toTypeName()).build())
|
||||||
.addFunction(
|
.addFunction(
|
||||||
FunSpec.builder("bind${ksClass.simpleName.asString()}Factory")
|
FunSpec.builder("bind${ksClass.simpleName.asString()}Factory")
|
||||||
.addModifiers(KModifier.ABSTRACT)
|
.addModifiers(KModifier.ABSTRACT)
|
||||||
|
|
@ -88,7 +91,7 @@ class ContributesNodeProcessor(
|
||||||
.addAnnotation(IntoMap::class)
|
.addAnnotation(IntoMap::class)
|
||||||
.addAnnotation(
|
.addAnnotation(
|
||||||
AnnotationSpec.Companion.builder(ClassName.bestGuess(nodeKeyFqName.asString())).addMember(
|
AnnotationSpec.Companion.builder(ClassName.bestGuess(nodeKeyFqName.asString())).addMember(
|
||||||
"%T::class",
|
CLASS_PLACEHOLDER,
|
||||||
ClassName.bestGuess(ksClass.qualifiedName!!.asString())
|
ClassName.bestGuess(ksClass.qualifiedName!!.asString())
|
||||||
).build()
|
).build()
|
||||||
)
|
)
|
||||||
|
|
@ -115,7 +118,7 @@ class ContributesNodeProcessor(
|
||||||
val assistedParameters = constructor.parameters.filter { it.isAnnotationPresent(Assisted::class) }
|
val assistedParameters = constructor.parameters.filter { it.isAnnotationPresent(Assisted::class) }
|
||||||
if (assistedParameters.size != 2) {
|
if (assistedParameters.size != 2) {
|
||||||
error(
|
error(
|
||||||
"${ksClass.qualifiedName?.asString()} must have an @Inject constructor with 2 @Assisted parameters. Found: ${assistedParameters.size}",
|
"${ksClass.qualifiedName?.asString()} must have a constructor with 2 @Assisted parameters. Found: ${assistedParameters.size}",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
val contextAssistedParam = assistedParameters[0]
|
val contextAssistedParam = assistedParameters[0]
|
||||||
|
|
@ -138,6 +141,7 @@ class ContributesNodeProcessor(
|
||||||
.addType(
|
.addType(
|
||||||
TypeSpec.interfaceBuilder(assistedFactoryClassName)
|
TypeSpec.interfaceBuilder(assistedFactoryClassName)
|
||||||
.addSuperinterface(ClassName.bestGuess(assistedNodeFactoryFqName.asString()).parameterizedBy(nodeClassName))
|
.addSuperinterface(ClassName.bestGuess(assistedNodeFactoryFqName.asString()).parameterizedBy(nodeClassName))
|
||||||
|
.addAnnotation(AnnotationSpec.builder(Origin::class).addMember("%T::class", nodeClassName).build())
|
||||||
.addAnnotation(AssistedFactory::class)
|
.addAnnotation(AssistedFactory::class)
|
||||||
.addFunction(
|
.addFunction(
|
||||||
FunSpec.builder("create")
|
FunSpec.builder("create")
|
||||||
|
|
@ -161,6 +165,7 @@ class ContributesNodeProcessor(
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
private const val CLASS_PLACEHOLDER = "%T::class"
|
||||||
private val assistedNodeFactoryFqName = FqName("io.element.android.libraries.architecture.AssistedNodeFactory")
|
private val assistedNodeFactoryFqName = FqName("io.element.android.libraries.architecture.AssistedNodeFactory")
|
||||||
private val nodeKeyFqName = FqName("io.element.android.libraries.architecture.NodeKey")
|
private val nodeKeyFqName = FqName("io.element.android.libraries.architecture.NodeKey")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
Subproject commit 95789d40119499eba8a79284df9dd2306405b099
|
Subproject commit ffc02b8d0f35188c3ef8a876dc1532bfe3e533da
|
||||||
2
fastlane/metadata/android/en-US/changelogs/202510000.txt
Normal file
2
fastlane/metadata/android/en-US/changelogs/202510000.txt
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
Main changes in this version: Spaces!
|
||||||
|
Full changelog: https://github.com/element-hq/element-x-android/releases
|
||||||
|
|
@ -16,14 +16,14 @@ import com.bumble.appyx.core.node.Node
|
||||||
import com.bumble.appyx.core.plugin.Plugin
|
import com.bumble.appyx.core.plugin.Plugin
|
||||||
import dev.zacsweers.metro.AppScope
|
import dev.zacsweers.metro.AppScope
|
||||||
import dev.zacsweers.metro.Assisted
|
import dev.zacsweers.metro.Assisted
|
||||||
import dev.zacsweers.metro.Inject
|
import dev.zacsweers.metro.AssistedInject
|
||||||
import io.element.android.annotations.ContributesNode
|
import io.element.android.annotations.ContributesNode
|
||||||
import io.element.android.appconfig.AnalyticsConfig
|
import io.element.android.appconfig.AnalyticsConfig
|
||||||
import io.element.android.compound.theme.ElementTheme
|
import io.element.android.compound.theme.ElementTheme
|
||||||
import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab
|
import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab
|
||||||
|
|
||||||
@ContributesNode(AppScope::class)
|
@ContributesNode(AppScope::class)
|
||||||
@Inject
|
@AssistedInject
|
||||||
class AnalyticsOptInNode(
|
class AnalyticsOptInNode(
|
||||||
@Assisted buildContext: BuildContext,
|
@Assisted buildContext: BuildContext,
|
||||||
@Assisted plugins: List<Plugin>,
|
@Assisted plugins: List<Plugin>,
|
||||||
|
|
|
||||||
13
features/announcement/api/build.gradle.kts
Normal file
13
features/announcement/api/build.gradle.kts
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
plugins {
|
||||||
|
id("io.element.android-compose-library")
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "io.element.android.features.announcement.api"
|
||||||
|
}
|
||||||
|
|
@ -5,8 +5,9 @@
|
||||||
* Please see LICENSE files in the repository root for full details.
|
* Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.features.space.impl
|
package io.element.android.features.announcement.api
|
||||||
|
|
||||||
sealed interface SpaceEvents {
|
enum class Announcement {
|
||||||
data object LoadMore : SpaceEvents
|
Space,
|
||||||
|
NewNotificationSound,
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
/*
|
||||||
|
* 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.announcement.api
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
interface AnnouncementService {
|
||||||
|
suspend fun showAnnouncement(announcement: Announcement)
|
||||||
|
|
||||||
|
suspend fun onAnnouncementDismissed(announcement: Announcement)
|
||||||
|
|
||||||
|
fun announcementsToShowFlow(): Flow<List<Announcement>>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use this composable to render the announcement UI in Fullscreen.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun Render(
|
||||||
|
modifier: Modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
37
features/announcement/impl/build.gradle.kts
Normal file
37
features/announcement/impl/build.gradle.kts
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
import extension.setupDependencyInjection
|
||||||
|
import extension.testCommonDependencies
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
id("io.element.android-compose-library")
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "io.element.android.features.announcement.impl"
|
||||||
|
|
||||||
|
testOptions {
|
||||||
|
unitTests {
|
||||||
|
isIncludeAndroidResources = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setupDependencyInjection()
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation(projects.libraries.architecture)
|
||||||
|
implementation(projects.libraries.designsystem)
|
||||||
|
implementation(projects.libraries.preferences.api)
|
||||||
|
implementation(projects.libraries.uiStrings)
|
||||||
|
api(projects.features.announcement.api)
|
||||||
|
implementation(libs.androidx.datastore.preferences)
|
||||||
|
|
||||||
|
testCommonDependencies(libs, true)
|
||||||
|
testImplementation(projects.libraries.matrix.test)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
/*
|
||||||
|
* 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.announcement.impl
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import dev.zacsweers.metro.Inject
|
||||||
|
import io.element.android.features.announcement.api.Announcement
|
||||||
|
import io.element.android.features.announcement.impl.store.AnnouncementStatus
|
||||||
|
import io.element.android.features.announcement.impl.store.AnnouncementStore
|
||||||
|
import io.element.android.libraries.architecture.Presenter
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
class AnnouncementPresenter(
|
||||||
|
private val announcementStore: AnnouncementStore,
|
||||||
|
) : Presenter<AnnouncementState> {
|
||||||
|
@Composable
|
||||||
|
override fun present(): AnnouncementState {
|
||||||
|
val showSpaceAnnouncement by remember {
|
||||||
|
announcementStore.announcementStatusFlow(Announcement.Space).map {
|
||||||
|
it == AnnouncementStatus.Show
|
||||||
|
}
|
||||||
|
}.collectAsState(false)
|
||||||
|
return AnnouncementState(
|
||||||
|
showSpaceAnnouncement = showSpaceAnnouncement,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
/*
|
||||||
|
* 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.announcement.impl
|
||||||
|
|
||||||
|
data class AnnouncementState(
|
||||||
|
val showSpaceAnnouncement: Boolean,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun anAnnouncementState(
|
||||||
|
showSpaceAnnouncement: Boolean = false,
|
||||||
|
) = AnnouncementState(
|
||||||
|
showSpaceAnnouncement = showSpaceAnnouncement,
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,90 @@
|
||||||
|
/*
|
||||||
|
* 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.announcement.impl
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.fadeIn
|
||||||
|
import androidx.compose.animation.fadeOut
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import dev.zacsweers.metro.AppScope
|
||||||
|
import dev.zacsweers.metro.ContributesBinding
|
||||||
|
import dev.zacsweers.metro.Inject
|
||||||
|
import io.element.android.features.announcement.api.Announcement
|
||||||
|
import io.element.android.features.announcement.api.AnnouncementService
|
||||||
|
import io.element.android.features.announcement.impl.spaces.SpaceAnnouncementState
|
||||||
|
import io.element.android.features.announcement.impl.spaces.SpaceAnnouncementView
|
||||||
|
import io.element.android.features.announcement.impl.store.AnnouncementStatus
|
||||||
|
import io.element.android.features.announcement.impl.store.AnnouncementStore
|
||||||
|
import io.element.android.libraries.architecture.Presenter
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
|
||||||
|
@ContributesBinding(AppScope::class)
|
||||||
|
@Inject
|
||||||
|
class DefaultAnnouncementService(
|
||||||
|
private val announcementStore: AnnouncementStore,
|
||||||
|
private val announcementPresenter: Presenter<AnnouncementState>,
|
||||||
|
private val spaceAnnouncementPresenter: Presenter<SpaceAnnouncementState>,
|
||||||
|
) : AnnouncementService {
|
||||||
|
override suspend fun showAnnouncement(announcement: Announcement) {
|
||||||
|
when (announcement) {
|
||||||
|
Announcement.Space -> showSpaceAnnouncement()
|
||||||
|
Announcement.NewNotificationSound -> {
|
||||||
|
announcementStore.setAnnouncementStatus(Announcement.NewNotificationSound, AnnouncementStatus.Show)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun onAnnouncementDismissed(announcement: Announcement) {
|
||||||
|
announcementStore.setAnnouncementStatus(announcement, AnnouncementStatus.Shown)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun announcementsToShowFlow(): Flow<List<Announcement>> {
|
||||||
|
return combine(
|
||||||
|
announcementStore.announcementStatusFlow(Announcement.Space),
|
||||||
|
announcementStore.announcementStatusFlow(Announcement.NewNotificationSound),
|
||||||
|
) { spaceAnnouncementStatus, newNotificationSoundStatus ->
|
||||||
|
buildList {
|
||||||
|
if (spaceAnnouncementStatus == AnnouncementStatus.Show) {
|
||||||
|
add(Announcement.Space)
|
||||||
|
}
|
||||||
|
if (newNotificationSoundStatus == AnnouncementStatus.Show) {
|
||||||
|
add(Announcement.NewNotificationSound)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun showSpaceAnnouncement() {
|
||||||
|
val currentValue = announcementStore.announcementStatusFlow(Announcement.Space).first()
|
||||||
|
if (currentValue == AnnouncementStatus.NeverShown) {
|
||||||
|
announcementStore.setAnnouncementStatus(Announcement.Space, AnnouncementStatus.Show)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun Render(modifier: Modifier) {
|
||||||
|
val announcementState = announcementPresenter.present()
|
||||||
|
Box(modifier = modifier.fillMaxSize()) {
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = announcementState.showSpaceAnnouncement,
|
||||||
|
enter = fadeIn(),
|
||||||
|
exit = fadeOut(),
|
||||||
|
) {
|
||||||
|
val spaceAnnouncementState = spaceAnnouncementPresenter.present()
|
||||||
|
SpaceAnnouncementView(
|
||||||
|
state = spaceAnnouncementState,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
/*
|
||||||
|
* 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.announcement.impl.di
|
||||||
|
|
||||||
|
import dev.zacsweers.metro.AppScope
|
||||||
|
import dev.zacsweers.metro.BindingContainer
|
||||||
|
import dev.zacsweers.metro.Binds
|
||||||
|
import dev.zacsweers.metro.ContributesTo
|
||||||
|
import io.element.android.features.announcement.impl.AnnouncementPresenter
|
||||||
|
import io.element.android.features.announcement.impl.AnnouncementState
|
||||||
|
import io.element.android.features.announcement.impl.spaces.SpaceAnnouncementPresenter
|
||||||
|
import io.element.android.features.announcement.impl.spaces.SpaceAnnouncementState
|
||||||
|
import io.element.android.libraries.architecture.Presenter
|
||||||
|
|
||||||
|
@ContributesTo(AppScope::class)
|
||||||
|
@BindingContainer
|
||||||
|
interface AnnouncementModule {
|
||||||
|
@Binds
|
||||||
|
fun bindAnnouncementPresenter(presenter: AnnouncementPresenter): Presenter<AnnouncementState>
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
fun bindSpaceAnnouncementPresenter(presenter: SpaceAnnouncementPresenter): Presenter<SpaceAnnouncementState>
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
/*
|
||||||
|
* 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.announcement.impl.spaces
|
||||||
|
|
||||||
|
sealed interface SpaceAnnouncementEvents {
|
||||||
|
data object Continue : SpaceAnnouncementEvents
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
/*
|
||||||
|
* 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.announcement.impl.spaces
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import dev.zacsweers.metro.Inject
|
||||||
|
import io.element.android.features.announcement.api.Announcement
|
||||||
|
import io.element.android.features.announcement.impl.store.AnnouncementStatus
|
||||||
|
import io.element.android.features.announcement.impl.store.AnnouncementStore
|
||||||
|
import io.element.android.libraries.architecture.Presenter
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
class SpaceAnnouncementPresenter(
|
||||||
|
private val announcementStore: AnnouncementStore,
|
||||||
|
) : Presenter<SpaceAnnouncementState> {
|
||||||
|
@Composable
|
||||||
|
override fun present(): SpaceAnnouncementState {
|
||||||
|
val localCoroutineScope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
fun handleEvents(event: SpaceAnnouncementEvents) {
|
||||||
|
when (event) {
|
||||||
|
SpaceAnnouncementEvents.Continue -> localCoroutineScope.launch {
|
||||||
|
announcementStore.setAnnouncementStatus(Announcement.Space, AnnouncementStatus.Shown)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return SpaceAnnouncementState(
|
||||||
|
eventSink = ::handleEvents
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
/*
|
||||||
|
* 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.announcement.impl.spaces
|
||||||
|
|
||||||
|
data class SpaceAnnouncementState(
|
||||||
|
val eventSink: (SpaceAnnouncementEvents) -> Unit
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
/*
|
||||||
|
* 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.announcement.impl.spaces
|
||||||
|
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
|
|
||||||
|
open class SpaceAnnouncementStateProvider : PreviewParameterProvider<SpaceAnnouncementState> {
|
||||||
|
override val values: Sequence<SpaceAnnouncementState>
|
||||||
|
get() = sequenceOf(
|
||||||
|
aSpaceAnnouncementState(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun aSpaceAnnouncementState(
|
||||||
|
eventSink: (SpaceAnnouncementEvents) -> Unit = {},
|
||||||
|
) = SpaceAnnouncementState(
|
||||||
|
eventSink = eventSink,
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,157 @@
|
||||||
|
/*
|
||||||
|
* 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.announcement.impl.spaces
|
||||||
|
|
||||||
|
import androidx.activity.compose.BackHandler
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import io.element.android.compound.theme.ElementTheme
|
||||||
|
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||||
|
import io.element.android.features.announcement.impl.R
|
||||||
|
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.preview.ElementPreview
|
||||||
|
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.Text
|
||||||
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ref: https://www.figma.com/design/kcnHxunG1LDWXsJhaNuiHz/ER-145--Spaces-on-Element-X?node-id=4593-40181
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun SpaceAnnouncementView(
|
||||||
|
state: SpaceAnnouncementState,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
val eventSink = state.eventSink
|
||||||
|
|
||||||
|
fun onContinue() {
|
||||||
|
eventSink(SpaceAnnouncementEvents.Continue)
|
||||||
|
}
|
||||||
|
|
||||||
|
BackHandler(onBack = ::onContinue)
|
||||||
|
HeaderFooterPage(
|
||||||
|
modifier = modifier,
|
||||||
|
isScrollable = true,
|
||||||
|
contentPadding = PaddingValues(top = 24.dp, start = 16.dp, end = 16.dp, bottom = 24.dp),
|
||||||
|
header = {
|
||||||
|
SpaceAnnouncementHeader()
|
||||||
|
},
|
||||||
|
content = {
|
||||||
|
SpaceAnnouncementContent(
|
||||||
|
modifier = Modifier.padding(horizontal = 8.dp),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
footer = {
|
||||||
|
SpaceAnnouncementFooter(
|
||||||
|
onContinue = ::onContinue,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun SpaceAnnouncementHeader(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
IconTitleSubtitleMolecule(
|
||||||
|
modifier = modifier.padding(top = 16.dp, bottom = 16.dp),
|
||||||
|
title = stringResource(id = R.string.screen_space_announcement_title),
|
||||||
|
showBetaLabel = true,
|
||||||
|
subTitle = stringResource(id = R.string.screen_space_announcement_subtitle),
|
||||||
|
iconStyle = BigIcon.Style.Default(
|
||||||
|
vectorIcon = CompoundIcons.WorkspaceSolid(),
|
||||||
|
usePrimaryTint = true,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun SpaceAnnouncementContent(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = modifier.fillMaxSize(),
|
||||||
|
) {
|
||||||
|
InfoListOrganism(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
items = persistentListOf(
|
||||||
|
InfoListItem(
|
||||||
|
message = stringResource(id = R.string.screen_space_announcement_item1),
|
||||||
|
iconVector = CompoundIcons.VisibilityOn(),
|
||||||
|
),
|
||||||
|
InfoListItem(
|
||||||
|
message = stringResource(id = R.string.screen_space_announcement_item2),
|
||||||
|
iconVector = CompoundIcons.Email(),
|
||||||
|
),
|
||||||
|
InfoListItem(
|
||||||
|
message = stringResource(id = R.string.screen_space_announcement_item3),
|
||||||
|
iconVector = CompoundIcons.Search(),
|
||||||
|
),
|
||||||
|
InfoListItem(
|
||||||
|
message = stringResource(id = R.string.screen_space_announcement_item4),
|
||||||
|
iconVector = CompoundIcons.Explore(),
|
||||||
|
),
|
||||||
|
InfoListItem(
|
||||||
|
message = stringResource(id = R.string.screen_space_announcement_item5),
|
||||||
|
iconVector = CompoundIcons.Leave(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
textStyle = ElementTheme.typography.fontBodyLgMedium,
|
||||||
|
iconTint = ElementTheme.colors.iconSecondary,
|
||||||
|
iconSize = 24.dp
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 16.dp),
|
||||||
|
text = stringResource(id = R.string.screen_space_announcement_notice),
|
||||||
|
style = ElementTheme.typography.fontBodyMdRegular,
|
||||||
|
color = ElementTheme.colors.textSecondary,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun SpaceAnnouncementFooter(
|
||||||
|
onContinue: () -> Unit,
|
||||||
|
) {
|
||||||
|
ButtonColumnMolecule(
|
||||||
|
modifier = Modifier.padding(bottom = 8.dp)
|
||||||
|
) {
|
||||||
|
Button(
|
||||||
|
text = stringResource(id = CommonStrings.action_continue),
|
||||||
|
onClick = onContinue,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreviewsDayNight
|
||||||
|
@Composable
|
||||||
|
internal fun SpaceAnnouncementViewPreview(@PreviewParameter(SpaceAnnouncementStateProvider::class) state: SpaceAnnouncementState) = ElementPreview {
|
||||||
|
SpaceAnnouncementView(
|
||||||
|
state = state,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
/*
|
||||||
|
* 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.announcement.impl.store
|
||||||
|
|
||||||
|
enum class AnnouncementStatus {
|
||||||
|
NeverShown,
|
||||||
|
Show,
|
||||||
|
Shown,
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
/*
|
||||||
|
* 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.announcement.impl.store
|
||||||
|
|
||||||
|
import io.element.android.features.announcement.api.Announcement
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
interface AnnouncementStore {
|
||||||
|
suspend fun setAnnouncementStatus(
|
||||||
|
announcement: Announcement,
|
||||||
|
status: AnnouncementStatus,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun announcementStatusFlow(
|
||||||
|
announcement: Announcement,
|
||||||
|
): Flow<AnnouncementStatus>
|
||||||
|
|
||||||
|
suspend fun reset()
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,58 @@
|
||||||
|
/*
|
||||||
|
* 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.announcement.impl.store
|
||||||
|
|
||||||
|
import androidx.datastore.preferences.core.edit
|
||||||
|
import androidx.datastore.preferences.core.intPreferencesKey
|
||||||
|
import dev.zacsweers.metro.AppScope
|
||||||
|
import dev.zacsweers.metro.ContributesBinding
|
||||||
|
import dev.zacsweers.metro.Inject
|
||||||
|
import io.element.android.features.announcement.api.Announcement
|
||||||
|
import io.element.android.libraries.preferences.api.store.PreferenceDataStoreFactory
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
|
||||||
|
private val spaceAnnouncementKey = intPreferencesKey("spaceAnnouncement")
|
||||||
|
private val newNotificationSoundKey = intPreferencesKey("newNotificationSound")
|
||||||
|
|
||||||
|
@ContributesBinding(AppScope::class)
|
||||||
|
@Inject
|
||||||
|
class DefaultAnnouncementStore(
|
||||||
|
preferenceDataStoreFactory: PreferenceDataStoreFactory,
|
||||||
|
) : AnnouncementStore {
|
||||||
|
private val store = preferenceDataStoreFactory.create("elementx_announcement")
|
||||||
|
|
||||||
|
override suspend fun setAnnouncementStatus(announcement: Announcement, status: AnnouncementStatus) {
|
||||||
|
val key = announcement.toKey()
|
||||||
|
store.edit { prefs ->
|
||||||
|
prefs[key] = status.ordinal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun announcementStatusFlow(announcement: Announcement): Flow<AnnouncementStatus> {
|
||||||
|
val key = announcement.toKey()
|
||||||
|
// For NewNotificationSound, a migration will set it to Show on application upgrade (see AppMigration08)
|
||||||
|
val defaultStatus = when (announcement) {
|
||||||
|
Announcement.Space -> AnnouncementStatus.NeverShown
|
||||||
|
Announcement.NewNotificationSound -> AnnouncementStatus.Shown
|
||||||
|
}
|
||||||
|
return store.data.map { prefs ->
|
||||||
|
val ordinal = prefs[key] ?: defaultStatus.ordinal
|
||||||
|
AnnouncementStatus.entries.getOrElse(ordinal) { defaultStatus }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun reset() {
|
||||||
|
store.edit { it.clear() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Announcement.toKey() = when (this) {
|
||||||
|
Announcement.Space -> spaceAnnouncementKey
|
||||||
|
Announcement.NewNotificationSound -> newNotificationSoundKey
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_space_announcement_item1">"Se klynger, du har oprettet eller tilmeldt dig"</string>
|
||||||
|
<string name="screen_space_announcement_item2">"Acceptere eller afvise invitationer til klynger"</string>
|
||||||
|
<string name="screen_space_announcement_item3">"Finde alle rum, du kan deltage i, i dine klynger"</string>
|
||||||
|
<string name="screen_space_announcement_item4">"Deltage i offentlige klynger"</string>
|
||||||
|
<string name="screen_space_announcement_item5">"Forlade de klynger, du har tilsluttet dig"</string>
|
||||||
|
<string name="screen_space_announcement_notice">"Oprettelse og administration af klynger kommer snart."</string>
|
||||||
|
<string name="screen_space_announcement_subtitle">"Velkommen til betaversionen af Klynger! Med denne første version kan du:"</string>
|
||||||
|
<string name="screen_space_announcement_title">"Introduktion til Klynger"</string>
|
||||||
|
</resources>
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_space_announcement_item1">"Von dir erstellte oder beigetretene Spaces anzeigen"</string>
|
||||||
|
<string name="screen_space_announcement_item2">"Einladungen zu Spaces annehmen oder ablehnen"</string>
|
||||||
|
<string name="screen_space_announcement_item3">"Chats innerhalb deiner Spaces entdecken, um ihnen beizutreten"</string>
|
||||||
|
<string name="screen_space_announcement_item4">"Öffentlichen Spaces beitreten"</string>
|
||||||
|
<string name="screen_space_announcement_item5">"Spaces verlassen, bei denen du Mitglied bist"</string>
|
||||||
|
<string name="screen_space_announcement_notice">"Das Erstellen und Verwalten von Spaces ist bald verfügbar."</string>
|
||||||
|
<string name="screen_space_announcement_subtitle">"Willkommen bei der Beta-Version von Spaces! Mit dieser ersten Version kannst du:"</string>
|
||||||
|
<string name="screen_space_announcement_title">"Einführung in Spaces"</string>
|
||||||
|
</resources>
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_space_announcement_item1">"Nähdä luomasi tai liittymäsi tilat"</string>
|
||||||
|
<string name="screen_space_announcement_item2">"Hyväksyä tai hylätä kutsuja tiloihin"</string>
|
||||||
|
<string name="screen_space_announcement_item3">"Löytää kaikki huoneet, joihin voit liittyä tiloissasi"</string>
|
||||||
|
<string name="screen_space_announcement_item4">"Liittyä julkisiin tiloihin"</string>
|
||||||
|
<string name="screen_space_announcement_item5">"Poistua mistä tahansa tilasta, johon olet liittynyt"</string>
|
||||||
|
<string name="screen_space_announcement_notice">"Tilojen luominen ja hallinta on tulossa pian."</string>
|
||||||
|
<string name="screen_space_announcement_subtitle">"Tervetuloa tilojen beetaversioon! Tämän ensimmäisen version avulla voit:"</string>
|
||||||
|
<string name="screen_space_announcement_title">"Esittelyssä tilat"</string>
|
||||||
|
</resources>
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_space_announcement_item1">"Voir les espaces que vous avez créés ou rejoints"</string>
|
||||||
|
<string name="screen_space_announcement_item2">"Accepter ou refuser les invitations aux espaces"</string>
|
||||||
|
<string name="screen_space_announcement_item3">"Découvrir les salons que vous pouvez joindre depuis vos espaces"</string>
|
||||||
|
<string name="screen_space_announcement_item4">"Rejoindre les espaces publics"</string>
|
||||||
|
<string name="screen_space_announcement_item5">"Quitter les espaces dont vous êtes membre."</string>
|
||||||
|
<string name="screen_space_announcement_notice">"La création et la gestion des espaces seront bientôt disponibles."</string>
|
||||||
|
<string name="screen_space_announcement_subtitle">"Bienvenue dans la version bêta des espaces! Avec cette première version, vous pourrez :"</string>
|
||||||
|
<string name="screen_space_announcement_title">"Ajout des espaces"</string>
|
||||||
|
</resources>
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_space_announcement_item1">"Se områder du har opprettet eller blitt med i"</string>
|
||||||
|
<string name="screen_space_announcement_item2">"Godta eller avslå invitasjoner til områder"</string>
|
||||||
|
<string name="screen_space_announcement_item3">"Oppdag alle rom du kan bli med i i dine områder"</string>
|
||||||
|
<string name="screen_space_announcement_item4">"Bli med i offentlige områder"</string>
|
||||||
|
<string name="screen_space_announcement_item5">"Forlat områder du har blitt med i"</string>
|
||||||
|
<string name="screen_space_announcement_notice">"Oppretting og administrasjon av områder kommer snart."</string>
|
||||||
|
<string name="screen_space_announcement_subtitle">"Velkommen til betaversjonen av Områder! Med denne første versjonen kan du:"</string>
|
||||||
|
<string name="screen_space_announcement_title">"Vi introduserer Områder"</string>
|
||||||
|
</resources>
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_space_announcement_item1">"Vizualizați spațiile pe care le-ați creat sau la care v-ați alăturat"</string>
|
||||||
|
<string name="screen_space_announcement_item2">"Acceptați sau refuzați invitațiile la spații"</string>
|
||||||
|
<string name="screen_space_announcement_item3">"Descoperiți toate camerele la care vă puteți alătura în spațiile dumneavoastră."</string>
|
||||||
|
<string name="screen_space_announcement_item4">"Alăturați-vă spațiilor publice"</string>
|
||||||
|
<string name="screen_space_announcement_item5">"Părăsiți spațiile la care v-ați alăturat."</string>
|
||||||
|
<string name="screen_space_announcement_notice">"Crearea și gestionarea spațiilor vor fi disponibile în curând."</string>
|
||||||
|
<string name="screen_space_announcement_subtitle">"Bun venit la versiunea beta a Spațiilor! Cu această primă versiune puteți:"</string>
|
||||||
|
<string name="screen_space_announcement_title">"Vă prezentăm Spații"</string>
|
||||||
|
</resources>
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_space_announcement_item1">"Просмотр пространств, которые вы создали или к которым присоединились"</string>
|
||||||
|
<string name="screen_space_announcement_item2">"Принимать или отклонять приглашения в пространства"</string>
|
||||||
|
<string name="screen_space_announcement_item3">"Откройте для себя все комнаты, к которым вы можете присоединиться в своих пространствах."</string>
|
||||||
|
<string name="screen_space_announcement_item4">"Присоединиться к публичному пространству"</string>
|
||||||
|
<string name="screen_space_announcement_item5">"Покинуть все пространства, к которым вы присоединились"</string>
|
||||||
|
<string name="screen_space_announcement_notice">"Создание и управление пространствами станет доступно в ближайшее время."</string>
|
||||||
|
<string name="screen_space_announcement_subtitle">"Добро пожаловать в бета-версию Spaces! В этой первой версии вы сможете:"</string>
|
||||||
|
<string name="screen_space_announcement_title">"Знакомство с пространствами"</string>
|
||||||
|
</resources>
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_space_announcement_item1">"查看您创建或加入的空间"</string>
|
||||||
|
<string name="screen_space_announcement_item2">"接受或拒绝空间邀请"</string>
|
||||||
|
</resources>
|
||||||
11
features/announcement/impl/src/main/res/values/localazy.xml
Normal file
11
features/announcement/impl/src/main/res/values/localazy.xml
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_space_announcement_item1">"View spaces you\'ve created or joined"</string>
|
||||||
|
<string name="screen_space_announcement_item2">"Accept or decline invites to spaces"</string>
|
||||||
|
<string name="screen_space_announcement_item3">"Discover any rooms you can join in your spaces"</string>
|
||||||
|
<string name="screen_space_announcement_item4">"Join public spaces"</string>
|
||||||
|
<string name="screen_space_announcement_item5">"Leave any spaces you’ve joined"</string>
|
||||||
|
<string name="screen_space_announcement_notice">"Filtering, creating and managing spaces is coming soon."</string>
|
||||||
|
<string name="screen_space_announcement_subtitle">"Welcome to the beta version of Spaces! With this first version you can:"</string>
|
||||||
|
<string name="screen_space_announcement_title">"Introducing Spaces"</string>
|
||||||
|
</resources>
|
||||||
|
|
@ -0,0 +1,52 @@
|
||||||
|
/*
|
||||||
|
* 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.announcement.impl
|
||||||
|
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import io.element.android.features.announcement.api.Announcement
|
||||||
|
import io.element.android.features.announcement.impl.store.AnnouncementStatus
|
||||||
|
import io.element.android.features.announcement.impl.store.AnnouncementStore
|
||||||
|
import io.element.android.features.announcement.impl.store.InMemoryAnnouncementStore
|
||||||
|
import io.element.android.tests.testutils.test
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class AnnouncementPresenterTest {
|
||||||
|
@Test
|
||||||
|
fun `present - initial state`() = runTest {
|
||||||
|
val presenter = createAnnouncementPresenter()
|
||||||
|
presenter.test {
|
||||||
|
val state = awaitItem()
|
||||||
|
assertThat(state.showSpaceAnnouncement).isFalse()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - showSpaceAnnouncement value depends on the value in the store`() = runTest {
|
||||||
|
val store = InMemoryAnnouncementStore()
|
||||||
|
val presenter = createAnnouncementPresenter(
|
||||||
|
announcementStore = store,
|
||||||
|
)
|
||||||
|
presenter.test {
|
||||||
|
val state = awaitItem()
|
||||||
|
assertThat(state.showSpaceAnnouncement).isFalse()
|
||||||
|
store.setAnnouncementStatus(Announcement.Space, AnnouncementStatus.Show)
|
||||||
|
val updatedState = awaitItem()
|
||||||
|
assertThat(updatedState.showSpaceAnnouncement).isTrue()
|
||||||
|
store.setAnnouncementStatus(Announcement.Space, AnnouncementStatus.Shown)
|
||||||
|
val finalState = awaitItem()
|
||||||
|
assertThat(finalState.showSpaceAnnouncement).isFalse()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createAnnouncementPresenter(
|
||||||
|
announcementStore: AnnouncementStore = InMemoryAnnouncementStore(),
|
||||||
|
) = AnnouncementPresenter(
|
||||||
|
announcementStore = announcementStore,
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,84 @@
|
||||||
|
/*
|
||||||
|
* 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.announcement.impl
|
||||||
|
|
||||||
|
import app.cash.turbine.test
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import io.element.android.features.announcement.api.Announcement
|
||||||
|
import io.element.android.features.announcement.impl.spaces.SpaceAnnouncementState
|
||||||
|
import io.element.android.features.announcement.impl.spaces.aSpaceAnnouncementState
|
||||||
|
import io.element.android.features.announcement.impl.store.AnnouncementStatus
|
||||||
|
import io.element.android.features.announcement.impl.store.AnnouncementStore
|
||||||
|
import io.element.android.features.announcement.impl.store.InMemoryAnnouncementStore
|
||||||
|
import io.element.android.libraries.architecture.Presenter
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class DefaultAnnouncementServiceTest {
|
||||||
|
@Test
|
||||||
|
fun `when showing Space announcement, space announcement is set to show only if it was never shown`() = runTest {
|
||||||
|
val announcementStore = InMemoryAnnouncementStore()
|
||||||
|
val sut = createDefaultAnnouncementService(
|
||||||
|
announcementStore = announcementStore,
|
||||||
|
)
|
||||||
|
assertThat(announcementStore.announcementStatusFlow(Announcement.Space).first()).isEqualTo(AnnouncementStatus.NeverShown)
|
||||||
|
sut.showAnnouncement(Announcement.Space)
|
||||||
|
assertThat(announcementStore.announcementStatusFlow(Announcement.Space).first()).isEqualTo(AnnouncementStatus.Show)
|
||||||
|
// Simulate user close the announcement
|
||||||
|
sut.onAnnouncementDismissed(Announcement.Space)
|
||||||
|
// Entering again the space tab should not change the value
|
||||||
|
sut.showAnnouncement(Announcement.Space)
|
||||||
|
assertThat(announcementStore.announcementStatusFlow(Announcement.Space).first()).isEqualTo(AnnouncementStatus.Shown)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `when showing NewNotificationSound announcement, announcement is set to show even if it was already shown`() = runTest {
|
||||||
|
val announcementStore = InMemoryAnnouncementStore()
|
||||||
|
val sut = createDefaultAnnouncementService(
|
||||||
|
announcementStore = announcementStore,
|
||||||
|
)
|
||||||
|
assertThat(announcementStore.announcementStatusFlow(Announcement.NewNotificationSound).first()).isEqualTo(AnnouncementStatus.NeverShown)
|
||||||
|
sut.showAnnouncement(Announcement.NewNotificationSound)
|
||||||
|
assertThat(announcementStore.announcementStatusFlow(Announcement.NewNotificationSound).first()).isEqualTo(AnnouncementStatus.Show)
|
||||||
|
// Simulate user close the announcement
|
||||||
|
sut.onAnnouncementDismissed(Announcement.NewNotificationSound)
|
||||||
|
// Calling again showAnnouncement should set it back to Show
|
||||||
|
sut.showAnnouncement(Announcement.NewNotificationSound)
|
||||||
|
assertThat(announcementStore.announcementStatusFlow(Announcement.NewNotificationSound).first()).isEqualTo(AnnouncementStatus.Show)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `test announcementsToShowFlow`() = runTest {
|
||||||
|
val announcementStore = InMemoryAnnouncementStore()
|
||||||
|
val sut = createDefaultAnnouncementService(
|
||||||
|
announcementStore = announcementStore,
|
||||||
|
)
|
||||||
|
sut.announcementsToShowFlow().test {
|
||||||
|
assertThat(awaitItem()).isEmpty()
|
||||||
|
announcementStore.setAnnouncementStatus(Announcement.Space, AnnouncementStatus.Show)
|
||||||
|
assertThat(awaitItem()).containsExactly(Announcement.Space)
|
||||||
|
announcementStore.setAnnouncementStatus(Announcement.NewNotificationSound, AnnouncementStatus.Show)
|
||||||
|
assertThat(awaitItem()).containsExactly(Announcement.Space, Announcement.NewNotificationSound)
|
||||||
|
announcementStore.setAnnouncementStatus(Announcement.Space, AnnouncementStatus.Shown)
|
||||||
|
assertThat(awaitItem()).containsExactly(Announcement.NewNotificationSound)
|
||||||
|
announcementStore.setAnnouncementStatus(Announcement.NewNotificationSound, AnnouncementStatus.Shown)
|
||||||
|
assertThat(awaitItem()).isEmpty()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createDefaultAnnouncementService(
|
||||||
|
announcementStore: AnnouncementStore = InMemoryAnnouncementStore(),
|
||||||
|
announcementPresenter: Presenter<AnnouncementState> = Presenter { anAnnouncementState() },
|
||||||
|
spaceAnnouncementPresenter: Presenter<SpaceAnnouncementState> = Presenter { aSpaceAnnouncementState() },
|
||||||
|
) = DefaultAnnouncementService(
|
||||||
|
announcementStore = announcementStore,
|
||||||
|
announcementPresenter = announcementPresenter,
|
||||||
|
spaceAnnouncementPresenter = spaceAnnouncementPresenter,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
/*
|
||||||
|
* 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.announcement.impl.spaces
|
||||||
|
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import io.element.android.features.announcement.api.Announcement
|
||||||
|
import io.element.android.features.announcement.impl.store.AnnouncementStatus
|
||||||
|
import io.element.android.features.announcement.impl.store.AnnouncementStore
|
||||||
|
import io.element.android.features.announcement.impl.store.InMemoryAnnouncementStore
|
||||||
|
import io.element.android.tests.testutils.test
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class SpaceAnnouncementPresenterTest {
|
||||||
|
@Test
|
||||||
|
fun `present - when user continues, the store is updated`() = runTest {
|
||||||
|
val store = InMemoryAnnouncementStore()
|
||||||
|
val presenter = createSpaceAnnouncementPresenter(
|
||||||
|
announcementStore = store,
|
||||||
|
)
|
||||||
|
presenter.test {
|
||||||
|
assertThat(store.announcementStatusFlow(Announcement.Space).first()).isEqualTo(AnnouncementStatus.NeverShown)
|
||||||
|
val state = awaitItem()
|
||||||
|
state.eventSink(SpaceAnnouncementEvents.Continue)
|
||||||
|
assertThat(store.announcementStatusFlow(Announcement.Space).first()).isEqualTo(AnnouncementStatus.Shown)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createSpaceAnnouncementPresenter(
|
||||||
|
announcementStore: AnnouncementStore = InMemoryAnnouncementStore(),
|
||||||
|
) = SpaceAnnouncementPresenter(
|
||||||
|
announcementStore = announcementStore,
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
/*
|
||||||
|
* 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.announcement.impl.spaces
|
||||||
|
|
||||||
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
|
||||||
|
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
import io.element.android.tests.testutils.EventsRecorder
|
||||||
|
import io.element.android.tests.testutils.clickOn
|
||||||
|
import io.element.android.tests.testutils.pressBackKey
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.rules.TestRule
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
class SpaceAnnouncementViewTest {
|
||||||
|
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `clicking on back sends a SpaceAnnouncementEvents`() {
|
||||||
|
val eventsRecorder = EventsRecorder<SpaceAnnouncementEvents>()
|
||||||
|
rule.setSpaceAnnouncementView(
|
||||||
|
aSpaceAnnouncementState(
|
||||||
|
eventSink = eventsRecorder,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
rule.pressBackKey()
|
||||||
|
eventsRecorder.assertSingle(SpaceAnnouncementEvents.Continue)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `clicking on Continue sends a SpaceAnnouncementEvents`() {
|
||||||
|
val eventsRecorder = EventsRecorder<SpaceAnnouncementEvents>()
|
||||||
|
rule.setSpaceAnnouncementView(
|
||||||
|
aSpaceAnnouncementState(
|
||||||
|
eventSink = eventsRecorder,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
rule.clickOn(CommonStrings.action_continue)
|
||||||
|
eventsRecorder.assertSingle(SpaceAnnouncementEvents.Continue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setSpaceAnnouncementView(
|
||||||
|
state: SpaceAnnouncementState,
|
||||||
|
) {
|
||||||
|
setContent {
|
||||||
|
SpaceAnnouncementView(
|
||||||
|
state = state,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
/*
|
||||||
|
* 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.announcement.impl.store
|
||||||
|
|
||||||
|
import io.element.android.features.announcement.api.Announcement
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
|
||||||
|
class InMemoryAnnouncementStore(
|
||||||
|
initialSpaceAnnouncementStatus: AnnouncementStatus = AnnouncementStatus.NeverShown,
|
||||||
|
initialNewNotificationSoundAnnouncementStatus: AnnouncementStatus = AnnouncementStatus.NeverShown,
|
||||||
|
) : AnnouncementStore {
|
||||||
|
private val spaceAnnouncement = MutableStateFlow(initialSpaceAnnouncementStatus)
|
||||||
|
private val newNotificationSoundAnnouncement = MutableStateFlow(initialNewNotificationSoundAnnouncementStatus)
|
||||||
|
|
||||||
|
override suspend fun setAnnouncementStatus(announcement: Announcement, status: AnnouncementStatus) {
|
||||||
|
announcement.toMutableStateFlow().value = status
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun announcementStatusFlow(announcement: Announcement): Flow<AnnouncementStatus> {
|
||||||
|
return announcement.toMutableStateFlow().asStateFlow()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun reset() {
|
||||||
|
spaceAnnouncement.value = AnnouncementStatus.NeverShown
|
||||||
|
newNotificationSoundAnnouncement.value = AnnouncementStatus.NeverShown
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Announcement.toMutableStateFlow() = when (this) {
|
||||||
|
Announcement.Space -> spaceAnnouncement
|
||||||
|
Announcement.NewNotificationSound -> newNotificationSoundAnnouncement
|
||||||
|
}
|
||||||
|
}
|
||||||
19
features/announcement/test/build.gradle.kts
Normal file
19
features/announcement/test/build.gradle.kts
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
plugins {
|
||||||
|
id("io.element.android-compose-library")
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "io.element.android.features.announcement.test"
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation(projects.features.announcement.api)
|
||||||
|
implementation(libs.coroutines.core)
|
||||||
|
implementation(projects.tests.testutils)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2024 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.rageshake.test.logs
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import io.element.android.features.announcement.api.Announcement
|
||||||
|
import io.element.android.features.announcement.api.AnnouncementService
|
||||||
|
import io.element.android.tests.testutils.lambda.lambdaError
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
|
||||||
|
class FakeAnnouncementService(
|
||||||
|
initialAnnouncementsToShowFlowValue: List<Announcement> = emptyList(),
|
||||||
|
val showAnnouncementResult: (Announcement) -> Unit = { lambdaError() },
|
||||||
|
val onAnnouncementDismissedResult: (Announcement) -> Unit = { lambdaError() },
|
||||||
|
val renderResult: (Modifier) -> Unit = { lambdaError() },
|
||||||
|
) : AnnouncementService {
|
||||||
|
private val announcementsToShowFlowValue = MutableStateFlow(initialAnnouncementsToShowFlowValue)
|
||||||
|
|
||||||
|
override suspend fun showAnnouncement(announcement: Announcement) {
|
||||||
|
showAnnouncementResult(announcement)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun onAnnouncementDismissed(announcement: Announcement) {
|
||||||
|
onAnnouncementDismissedResult(announcement)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun announcementsToShowFlow(): Flow<List<Announcement>> {
|
||||||
|
return announcementsToShowFlowValue.asStateFlow()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun emitAnnouncementsToShow(value: List<Announcement>) {
|
||||||
|
announcementsToShowFlowValue.value = value
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun Render(modifier: Modifier) {
|
||||||
|
renderResult(modifier)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -19,7 +19,7 @@ import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import dev.zacsweers.metro.Assisted
|
import dev.zacsweers.metro.Assisted
|
||||||
import dev.zacsweers.metro.AssistedFactory
|
import dev.zacsweers.metro.AssistedFactory
|
||||||
import dev.zacsweers.metro.Inject
|
import dev.zacsweers.metro.AssistedInject
|
||||||
import im.vector.app.features.analytics.plan.MobileScreen
|
import im.vector.app.features.analytics.plan.MobileScreen
|
||||||
import io.element.android.compound.theme.ElementTheme
|
import io.element.android.compound.theme.ElementTheme
|
||||||
import io.element.android.features.call.api.CallType
|
import io.element.android.features.call.api.CallType
|
||||||
|
|
@ -49,7 +49,7 @@ import timber.log.Timber
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import kotlin.time.Duration.Companion.seconds
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
@Inject
|
@AssistedInject
|
||||||
class CallScreenPresenter(
|
class CallScreenPresenter(
|
||||||
@Assisted private val callType: CallType,
|
@Assisted private val callType: CallType,
|
||||||
@Assisted private val navigator: CallScreenNavigator,
|
@Assisted private val navigator: CallScreenNavigator,
|
||||||
|
|
@ -242,7 +242,7 @@ class CallScreenPresenter(
|
||||||
}
|
}
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
Timber.d("Observing sync state in-call for sessionId: ${roomCallType.sessionId}")
|
Timber.d("Observing sync state in-call for sessionId: ${roomCallType.sessionId}")
|
||||||
client.syncService().syncState
|
client.syncService.syncState
|
||||||
.collect { state ->
|
.collect { state ->
|
||||||
if (state != SyncState.Running) {
|
if (state != SyncState.Running) {
|
||||||
appForegroundStateService.updateIsInCallState(true)
|
appForegroundStateService.updateIsInCallState(true)
|
||||||
|
|
|
||||||
|
|
@ -133,6 +133,7 @@ class WebViewWidgetMessageInterceptor(
|
||||||
return assetLoader.shouldInterceptRequest(request.url)
|
return assetLoader.shouldInterceptRequest(request.url)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("OVERRIDE_DEPRECATION")
|
||||||
override fun shouldInterceptRequest(view: WebView?, url: String): WebResourceResponse? {
|
override fun shouldInterceptRequest(view: WebView?, url: String): WebResourceResponse? {
|
||||||
return assetLoader.shouldInterceptRequest(url.toUri())
|
return assetLoader.shouldInterceptRequest(url.toUri())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ import com.bumble.appyx.core.modality.BuildContext
|
||||||
import com.bumble.appyx.core.node.Node
|
import com.bumble.appyx.core.node.Node
|
||||||
import com.bumble.appyx.core.plugin.Plugin
|
import com.bumble.appyx.core.plugin.Plugin
|
||||||
import dev.zacsweers.metro.Assisted
|
import dev.zacsweers.metro.Assisted
|
||||||
import dev.zacsweers.metro.Inject
|
import dev.zacsweers.metro.AssistedInject
|
||||||
import io.element.android.annotations.ContributesNode
|
import io.element.android.annotations.ContributesNode
|
||||||
import io.element.android.features.changeroommemberroes.api.ChangeRoomMemberRolesListType
|
import io.element.android.features.changeroommemberroes.api.ChangeRoomMemberRolesListType
|
||||||
import io.element.android.libraries.architecture.NodeInputs
|
import io.element.android.libraries.architecture.NodeInputs
|
||||||
|
|
@ -26,7 +26,7 @@ import io.element.android.libraries.matrix.api.room.RoomMember
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
|
|
||||||
@ContributesNode(RoomScope::class)
|
@ContributesNode(RoomScope::class)
|
||||||
@Inject
|
@AssistedInject
|
||||||
class ChangeRolesNode(
|
class ChangeRolesNode(
|
||||||
@Assisted buildContext: BuildContext,
|
@Assisted buildContext: BuildContext,
|
||||||
@Assisted plugins: List<Plugin>,
|
@Assisted plugins: List<Plugin>,
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import dev.zacsweers.metro.Assisted
|
import dev.zacsweers.metro.Assisted
|
||||||
import dev.zacsweers.metro.AssistedFactory
|
import dev.zacsweers.metro.AssistedFactory
|
||||||
import dev.zacsweers.metro.Inject
|
import dev.zacsweers.metro.AssistedInject
|
||||||
import im.vector.app.features.analytics.plan.RoomModeration
|
import im.vector.app.features.analytics.plan.RoomModeration
|
||||||
import io.element.android.libraries.architecture.AsyncAction
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
|
|
@ -37,17 +37,15 @@ import io.element.android.libraries.matrix.ui.model.roleOf
|
||||||
import io.element.android.libraries.matrix.ui.room.PowerLevelRoomMemberComparator
|
import io.element.android.libraries.matrix.ui.room.PowerLevelRoomMemberComparator
|
||||||
import io.element.android.services.analytics.api.AnalyticsService
|
import io.element.android.services.analytics.api.AnalyticsService
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.PersistentList
|
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import kotlinx.collections.immutable.toPersistentList
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@Inject
|
@AssistedInject
|
||||||
class ChangeRolesPresenter(
|
class ChangeRolesPresenter(
|
||||||
@Assisted private val role: RoomMember.Role,
|
@Assisted private val role: RoomMember.Role,
|
||||||
private val room: JoinedRoom,
|
private val room: JoinedRoom,
|
||||||
|
|
@ -73,11 +71,11 @@ class ChangeRolesPresenter(
|
||||||
}
|
}
|
||||||
val exitState: MutableState<AsyncAction<Unit>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
|
val exitState: MutableState<AsyncAction<Unit>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
|
||||||
val saveState: MutableState<AsyncAction<Unit>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
|
val saveState: MutableState<AsyncAction<Unit>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
|
||||||
val usersWithRole = produceState(initialValue = persistentListOf()) {
|
val usersWithRole = produceState<ImmutableList<MatrixUser>>(initialValue = persistentListOf()) {
|
||||||
room.usersWithRole(role).map { members -> members.map { it.toMatrixUser() } }
|
room.usersWithRole(role).map { members -> members.map { it.toMatrixUser() } }
|
||||||
.onEach { users ->
|
.onEach { users ->
|
||||||
val previous: PersistentList<MatrixUser> = value
|
val previous = value
|
||||||
value = users.toPersistentList()
|
value = users.toImmutableList()
|
||||||
// Users who were selected but didn't have the role, so their role change was pending
|
// Users who were selected but didn't have the role, so their role change was pending
|
||||||
val toAdd = selectedUsers.value.filter { user -> users.none { it.userId == user.userId } && previous.none { it.userId == user.userId } }
|
val toAdd = selectedUsers.value.filter { user -> users.none { it.userId == user.userId } && previous.none { it.userId == user.userId } }
|
||||||
// Users who no longer have the role
|
// Users who no longer have the role
|
||||||
|
|
|
||||||
|
|
@ -17,9 +17,9 @@ import com.bumble.appyx.core.node.Node
|
||||||
import com.bumble.appyx.core.node.ParentNode
|
import com.bumble.appyx.core.node.ParentNode
|
||||||
import com.bumble.appyx.core.plugin.Plugin
|
import com.bumble.appyx.core.plugin.Plugin
|
||||||
import dev.zacsweers.metro.Assisted
|
import dev.zacsweers.metro.Assisted
|
||||||
import dev.zacsweers.metro.Inject
|
import dev.zacsweers.metro.AssistedInject
|
||||||
import io.element.android.annotations.ContributesNode
|
import io.element.android.annotations.ContributesNode
|
||||||
import io.element.android.appnav.di.RoomComponentFactory
|
import io.element.android.appnav.di.RoomGraphFactory
|
||||||
import io.element.android.features.changeroommemberroes.api.ChangeRoomMemberRolesEntryPoint
|
import io.element.android.features.changeroommemberroes.api.ChangeRoomMemberRolesEntryPoint
|
||||||
import io.element.android.features.changeroommemberroes.api.ChangeRoomMemberRolesListType
|
import io.element.android.features.changeroommemberroes.api.ChangeRoomMemberRolesListType
|
||||||
import io.element.android.libraries.architecture.NodeInputs
|
import io.element.android.libraries.architecture.NodeInputs
|
||||||
|
|
@ -32,11 +32,11 @@ import io.element.android.libraries.matrix.api.room.JoinedRoom
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
@ContributesNode(SessionScope::class)
|
@ContributesNode(SessionScope::class)
|
||||||
@Inject
|
@AssistedInject
|
||||||
class ChangeRoomMemberRolesRootNode(
|
class ChangeRoomMemberRolesRootNode(
|
||||||
@Assisted buildContext: BuildContext,
|
@Assisted buildContext: BuildContext,
|
||||||
@Assisted plugins: List<Plugin>,
|
@Assisted plugins: List<Plugin>,
|
||||||
roomComponentFactory: RoomComponentFactory,
|
roomGraphFactory: RoomGraphFactory,
|
||||||
) : ParentNode<ChangeRoomMemberRolesRootNode.NavTarget>(
|
) : ParentNode<ChangeRoomMemberRolesRootNode.NavTarget>(
|
||||||
navModel = PermanentNavModel(
|
navModel = PermanentNavModel(
|
||||||
navTargets = setOf(NavTarget),
|
navTargets = setOf(NavTarget),
|
||||||
|
|
@ -54,7 +54,7 @@ class ChangeRoomMemberRolesRootNode(
|
||||||
|
|
||||||
private val inputs = inputs<Inputs>()
|
private val inputs = inputs<Inputs>()
|
||||||
|
|
||||||
override val graph = roomComponentFactory.create(inputs.joinedRoom)
|
override val graph = roomGraphFactory.create(inputs.joinedRoom)
|
||||||
|
|
||||||
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||||
return createNode<ChangeRolesNode>(
|
return createNode<ChangeRolesNode>(
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@
|
||||||
<string name="screen_room_change_role_administrators_title">"Rediger administratorer"</string>
|
<string name="screen_room_change_role_administrators_title">"Rediger administratorer"</string>
|
||||||
<string name="screen_room_change_role_confirm_add_admin_description">"Du vil ikke kunne angre denne handlingen. Du forfremmer brukeren til å ha samme rettighetsnivå som deg."</string>
|
<string name="screen_room_change_role_confirm_add_admin_description">"Du vil ikke kunne angre denne handlingen. Du forfremmer brukeren til å ha samme rettighetsnivå som deg."</string>
|
||||||
<string name="screen_room_change_role_confirm_add_admin_title">"Legg til administrator?"</string>
|
<string name="screen_room_change_role_confirm_add_admin_title">"Legg til administrator?"</string>
|
||||||
|
<string name="screen_room_change_role_confirm_change_owners_description">"Du kan ikke angre denne handlingen. Du overfører eierskapet til de valgte brukerne. Når du forlater siden, vil dette være permanent."</string>
|
||||||
<string name="screen_room_change_role_confirm_change_owners_title">"Overføre eierskapet?"</string>
|
<string name="screen_room_change_role_confirm_change_owners_title">"Overføre eierskapet?"</string>
|
||||||
<string name="screen_room_change_role_confirm_demote_self_action">"Degradere"</string>
|
<string name="screen_room_change_role_confirm_demote_self_action">"Degradere"</string>
|
||||||
<string name="screen_room_change_role_confirm_demote_self_description">"Du vil ikke kunne angre denne endringen ettersom du degraderer deg selv, og hvis du er den siste privilegerte brukeren i rommet, vil det være umulig å få tilbake privilegiene."</string>
|
<string name="screen_room_change_role_confirm_demote_self_description">"Du vil ikke kunne angre denne endringen ettersom du degraderer deg selv, og hvis du er den siste privilegerte brukeren i rommet, vil det være umulig å få tilbake privilegiene."</string>
|
||||||
|
|
|
||||||
|
|
@ -32,8 +32,8 @@ import io.element.android.libraries.previewutils.room.aRoomMemberList
|
||||||
import io.element.android.services.analytics.test.FakeAnalyticsService
|
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||||
import kotlinx.collections.immutable.persistentMapOf
|
import kotlinx.collections.immutable.persistentMapOf
|
||||||
import kotlinx.collections.immutable.toPersistentList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import kotlinx.collections.immutable.toPersistentMap
|
import kotlinx.collections.immutable.toImmutableMap
|
||||||
import kotlinx.coroutines.test.TestScope
|
import kotlinx.coroutines.test.TestScope
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
@ -103,7 +103,7 @@ class ChangeRolesPresenterTest {
|
||||||
// Owner - creator
|
// Owner - creator
|
||||||
aRoomMember(userId = creatorUserId, role = RoomMember.Role.Owner(isCreator = true))
|
aRoomMember(userId = creatorUserId, role = RoomMember.Role.Owner(isCreator = true))
|
||||||
)
|
)
|
||||||
givenRoomMembersState(RoomMembersState.Ready(roomMemberList.toPersistentList()))
|
givenRoomMembersState(RoomMembersState.Ready(roomMemberList.toImmutableList()))
|
||||||
}
|
}
|
||||||
val presenter = createChangeRolesPresenter(room = room)
|
val presenter = createChangeRolesPresenter(room = room)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
|
@ -124,7 +124,7 @@ class ChangeRolesPresenterTest {
|
||||||
val creatorUserId = UserId("@creator:matrix.org")
|
val creatorUserId = UserId("@creator:matrix.org")
|
||||||
val memberList = aRoomMemberList()
|
val memberList = aRoomMemberList()
|
||||||
.plus(aRoomMember(displayName = "CREATOR", role = RoomMember.Role.Owner(isCreator = true), userId = creatorUserId))
|
.plus(aRoomMember(displayName = "CREATOR", role = RoomMember.Role.Owner(isCreator = true), userId = creatorUserId))
|
||||||
.toPersistentList()
|
.toImmutableList()
|
||||||
givenRoomInfo(aRoomInfo(roomCreators = listOf(creatorUserId)))
|
givenRoomInfo(aRoomInfo(roomCreators = listOf(creatorUserId)))
|
||||||
givenRoomMembersState(RoomMembersState.Ready(memberList))
|
givenRoomMembersState(RoomMembersState.Ready(memberList))
|
||||||
}
|
}
|
||||||
|
|
@ -203,7 +203,7 @@ class ChangeRolesPresenterTest {
|
||||||
assertThat(initialResults?.moderators).hasSize(1)
|
assertThat(initialResults?.moderators).hasSize(1)
|
||||||
assertThat(initialResults?.admins).hasSize(1)
|
assertThat(initialResults?.admins).hasSize(1)
|
||||||
|
|
||||||
room.givenRoomMembersState(RoomMembersState.Ready(aRoomMemberList().take(1).toPersistentList()))
|
room.givenRoomMembersState(RoomMembersState.Ready(aRoomMemberList().take(1).toImmutableList()))
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
|
|
||||||
val searchResults = (awaitItem().searchResults as? SearchBarResultState.Results)?.results
|
val searchResults = (awaitItem().searchResults as? SearchBarResultState.Results)?.results
|
||||||
|
|
@ -552,7 +552,7 @@ class ChangeRolesPresenterTest {
|
||||||
private fun roomPowerLevelsWithRoles(vararg pairs: Pair<UserId, RoomMember.Role>): RoomPowerLevels {
|
private fun roomPowerLevelsWithRoles(vararg pairs: Pair<UserId, RoomMember.Role>): RoomPowerLevels {
|
||||||
return RoomPowerLevels(
|
return RoomPowerLevels(
|
||||||
values = defaultRoomPowerLevelValues(),
|
values = defaultRoomPowerLevelValues(),
|
||||||
users = pairs.associate { (userId, role) -> userId to role.powerLevel }.toPersistentMap()
|
users = pairs.associate { (userId, role) -> userId to role.powerLevel }.toImmutableMap()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ class DefaultChangeRoomMemberRolesEntyPointTest {
|
||||||
ChangeRoomMemberRolesRootNode(
|
ChangeRoomMemberRolesRootNode(
|
||||||
buildContext = buildContext,
|
buildContext = buildContext,
|
||||||
plugins = plugins,
|
plugins = plugins,
|
||||||
roomComponentFactory = { },
|
roomGraphFactory = { },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
val room = FakeJoinedRoom()
|
val room = FakeJoinedRoom()
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ import com.bumble.appyx.core.plugin.plugins
|
||||||
import com.bumble.appyx.navmodel.backstack.BackStack
|
import com.bumble.appyx.navmodel.backstack.BackStack
|
||||||
import com.bumble.appyx.navmodel.backstack.operation.replace
|
import com.bumble.appyx.navmodel.backstack.operation.replace
|
||||||
import dev.zacsweers.metro.Assisted
|
import dev.zacsweers.metro.Assisted
|
||||||
import dev.zacsweers.metro.Inject
|
import dev.zacsweers.metro.AssistedInject
|
||||||
import io.element.android.annotations.ContributesNode
|
import io.element.android.annotations.ContributesNode
|
||||||
import io.element.android.features.createroom.api.CreateRoomEntryPoint
|
import io.element.android.features.createroom.api.CreateRoomEntryPoint
|
||||||
import io.element.android.features.createroom.impl.addpeople.AddPeopleNode
|
import io.element.android.features.createroom.impl.addpeople.AddPeopleNode
|
||||||
|
|
@ -30,7 +30,7 @@ import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
@ContributesNode(SessionScope::class)
|
@ContributesNode(SessionScope::class)
|
||||||
@Inject
|
@AssistedInject
|
||||||
class CreateRoomFlowNode(
|
class CreateRoomFlowNode(
|
||||||
@Assisted buildContext: BuildContext,
|
@Assisted buildContext: BuildContext,
|
||||||
@Assisted plugins: List<Plugin>,
|
@Assisted plugins: List<Plugin>,
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ import com.bumble.appyx.core.node.Node
|
||||||
import com.bumble.appyx.core.plugin.Plugin
|
import com.bumble.appyx.core.plugin.Plugin
|
||||||
import com.bumble.appyx.core.plugin.plugins
|
import com.bumble.appyx.core.plugin.plugins
|
||||||
import dev.zacsweers.metro.Assisted
|
import dev.zacsweers.metro.Assisted
|
||||||
import dev.zacsweers.metro.Inject
|
import dev.zacsweers.metro.AssistedInject
|
||||||
import io.element.android.annotations.ContributesNode
|
import io.element.android.annotations.ContributesNode
|
||||||
import io.element.android.features.invitepeople.api.InvitePeoplePresenter
|
import io.element.android.features.invitepeople.api.InvitePeoplePresenter
|
||||||
import io.element.android.features.invitepeople.api.InvitePeopleRenderer
|
import io.element.android.features.invitepeople.api.InvitePeopleRenderer
|
||||||
|
|
@ -24,7 +24,7 @@ import io.element.android.libraries.di.SessionScope
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
|
||||||
@ContributesNode(SessionScope::class)
|
@ContributesNode(SessionScope::class)
|
||||||
@Inject
|
@AssistedInject
|
||||||
class AddPeopleNode(
|
class AddPeopleNode(
|
||||||
@Assisted buildContext: BuildContext,
|
@Assisted buildContext: BuildContext,
|
||||||
@Assisted plugins: List<Plugin>,
|
@Assisted plugins: List<Plugin>,
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ import com.bumble.appyx.core.node.Node
|
||||||
import com.bumble.appyx.core.plugin.Plugin
|
import com.bumble.appyx.core.plugin.Plugin
|
||||||
import com.bumble.appyx.core.plugin.plugins
|
import com.bumble.appyx.core.plugin.plugins
|
||||||
import dev.zacsweers.metro.Assisted
|
import dev.zacsweers.metro.Assisted
|
||||||
import dev.zacsweers.metro.Inject
|
import dev.zacsweers.metro.AssistedInject
|
||||||
import im.vector.app.features.analytics.plan.MobileScreen
|
import im.vector.app.features.analytics.plan.MobileScreen
|
||||||
import io.element.android.annotations.ContributesNode
|
import io.element.android.annotations.ContributesNode
|
||||||
import io.element.android.libraries.di.SessionScope
|
import io.element.android.libraries.di.SessionScope
|
||||||
|
|
@ -23,7 +23,7 @@ import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.services.analytics.api.AnalyticsService
|
import io.element.android.services.analytics.api.AnalyticsService
|
||||||
|
|
||||||
@ContributesNode(SessionScope::class)
|
@ContributesNode(SessionScope::class)
|
||||||
@Inject
|
@AssistedInject
|
||||||
class ConfigureRoomNode(
|
class ConfigureRoomNode(
|
||||||
@Assisted buildContext: BuildContext,
|
@Assisted buildContext: BuildContext,
|
||||||
@Assisted plugins: List<Plugin>,
|
@Assisted plugins: List<Plugin>,
|
||||||
|
|
|
||||||
|
|
@ -13,12 +13,12 @@ import com.bumble.appyx.core.modality.BuildContext
|
||||||
import com.bumble.appyx.core.node.Node
|
import com.bumble.appyx.core.node.Node
|
||||||
import com.bumble.appyx.core.plugin.Plugin
|
import com.bumble.appyx.core.plugin.Plugin
|
||||||
import dev.zacsweers.metro.Assisted
|
import dev.zacsweers.metro.Assisted
|
||||||
import dev.zacsweers.metro.Inject
|
import dev.zacsweers.metro.AssistedInject
|
||||||
import io.element.android.annotations.ContributesNode
|
import io.element.android.annotations.ContributesNode
|
||||||
import io.element.android.libraries.di.SessionScope
|
import io.element.android.libraries.di.SessionScope
|
||||||
|
|
||||||
@ContributesNode(SessionScope::class)
|
@ContributesNode(SessionScope::class)
|
||||||
@Inject
|
@AssistedInject
|
||||||
class AccountDeactivationNode(
|
class AccountDeactivationNode(
|
||||||
@Assisted buildContext: BuildContext,
|
@Assisted buildContext: BuildContext,
|
||||||
@Assisted plugins: List<Plugin>,
|
@Assisted plugins: List<Plugin>,
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
<string name="screen_deactivate_account_list_item_1">"%1$s a fiókját (nem fog tudni újra bejelentkezni, és az azonosítója nem használható újra)."</string>
|
<string name="screen_deactivate_account_list_item_1">"%1$s a fiókját (nem fog tudni újra bejelentkezni, és az azonosítója nem használható újra)."</string>
|
||||||
<string name="screen_deactivate_account_list_item_1_bold_part">"Véglegesen letiltja"</string>
|
<string name="screen_deactivate_account_list_item_1_bold_part">"Véglegesen letiltja"</string>
|
||||||
<string name="screen_deactivate_account_list_item_2">"Eltávolításra kerül az összes csevegőszobából."</string>
|
<string name="screen_deactivate_account_list_item_2">"Eltávolításra kerül az összes csevegőszobából."</string>
|
||||||
<string name="screen_deactivate_account_list_item_3">"Törlésre kerülnek a fiókadatai a személyazonosító kiszolgálónkról."</string>
|
<string name="screen_deactivate_account_list_item_3">"Törlésre kerülnek a fiókadatai az azonosítási kiszolgálónkról."</string>
|
||||||
<string name="screen_deactivate_account_list_item_4">"Üzenetei továbbra is láthatóak maradnak a regisztrált felhasználók számára, de nem lesznek elérhetőek az új vagy nem regisztrált felhasználók számára, ha úgy dönt, hogy törli őket."</string>
|
<string name="screen_deactivate_account_list_item_4">"Üzenetei továbbra is láthatóak maradnak a regisztrált felhasználók számára, de nem lesznek elérhetőek az új vagy nem regisztrált felhasználók számára, ha úgy dönt, hogy törli őket."</string>
|
||||||
<string name="screen_deactivate_account_title">"Fiók deaktiválása"</string>
|
<string name="screen_deactivate_account_title">"Fiók deaktiválása"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ android {
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(libs.compound)
|
implementation(projects.libraries.compound)
|
||||||
implementation(projects.libraries.architecture)
|
implementation(projects.libraries.architecture)
|
||||||
implementation(projects.libraries.matrix.api)
|
implementation(projects.libraries.matrix.api)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ android {
|
||||||
setupDependencyInjection()
|
setupDependencyInjection()
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(libs.compound)
|
implementation(projects.libraries.compound)
|
||||||
api(projects.features.enterprise.api)
|
api(projects.features.enterprise.api)
|
||||||
implementation(projects.libraries.architecture)
|
implementation(projects.libraries.architecture)
|
||||||
implementation(projects.libraries.matrix.api)
|
implementation(projects.libraries.matrix.api)
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ android {
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
api(projects.features.enterprise.api)
|
api(projects.features.enterprise.api)
|
||||||
implementation(libs.compound)
|
implementation(projects.libraries.compound)
|
||||||
implementation(projects.libraries.matrix.api)
|
implementation(projects.libraries.matrix.api)
|
||||||
implementation(projects.tests.testutils)
|
implementation(projects.tests.testutils)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@ dependencies {
|
||||||
implementation(projects.libraries.matrixui)
|
implementation(projects.libraries.matrixui)
|
||||||
implementation(projects.libraries.designsystem)
|
implementation(projects.libraries.designsystem)
|
||||||
implementation(projects.libraries.preferences.api)
|
implementation(projects.libraries.preferences.api)
|
||||||
|
implementation(projects.libraries.uiCommon)
|
||||||
implementation(projects.libraries.uiStrings)
|
implementation(projects.libraries.uiStrings)
|
||||||
implementation(projects.libraries.testtags)
|
implementation(projects.libraries.testtags)
|
||||||
implementation(projects.features.analytics.api)
|
implementation(projects.features.analytics.api)
|
||||||
|
|
|
||||||
|
|
@ -8,10 +8,7 @@
|
||||||
package io.element.android.features.ftue.impl
|
package io.element.android.features.ftue.impl
|
||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.bumble.appyx.core.modality.BuildContext
|
import com.bumble.appyx.core.modality.BuildContext
|
||||||
|
|
@ -20,9 +17,8 @@ import com.bumble.appyx.core.plugin.Plugin
|
||||||
import com.bumble.appyx.navmodel.backstack.BackStack
|
import com.bumble.appyx.navmodel.backstack.BackStack
|
||||||
import com.bumble.appyx.navmodel.backstack.operation.newRoot
|
import com.bumble.appyx.navmodel.backstack.operation.newRoot
|
||||||
import com.bumble.appyx.navmodel.backstack.operation.replace
|
import com.bumble.appyx.navmodel.backstack.operation.replace
|
||||||
import dev.zacsweers.metro.AppScope
|
|
||||||
import dev.zacsweers.metro.Assisted
|
import dev.zacsweers.metro.Assisted
|
||||||
import dev.zacsweers.metro.Inject
|
import dev.zacsweers.metro.AssistedInject
|
||||||
import io.element.android.annotations.ContributesNode
|
import io.element.android.annotations.ContributesNode
|
||||||
import io.element.android.features.analytics.api.AnalyticsEntryPoint
|
import io.element.android.features.analytics.api.AnalyticsEntryPoint
|
||||||
import io.element.android.features.ftue.impl.notifications.NotificationsOptInNode
|
import io.element.android.features.ftue.impl.notifications.NotificationsOptInNode
|
||||||
|
|
@ -34,15 +30,15 @@ import io.element.android.features.lockscreen.api.LockScreenEntryPoint
|
||||||
import io.element.android.libraries.architecture.BackstackView
|
import io.element.android.libraries.architecture.BackstackView
|
||||||
import io.element.android.libraries.architecture.BaseFlowNode
|
import io.element.android.libraries.architecture.BaseFlowNode
|
||||||
import io.element.android.libraries.architecture.createNode
|
import io.element.android.libraries.architecture.createNode
|
||||||
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
|
|
||||||
import io.element.android.libraries.di.SessionScope
|
import io.element.android.libraries.di.SessionScope
|
||||||
|
import io.element.android.libraries.ui.common.nodes.emptyNode
|
||||||
import kotlinx.coroutines.flow.filterIsInstance
|
import kotlinx.coroutines.flow.filterIsInstance
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
@ContributesNode(SessionScope::class)
|
@ContributesNode(SessionScope::class)
|
||||||
@Inject
|
@AssistedInject
|
||||||
class FtueFlowNode(
|
class FtueFlowNode(
|
||||||
@Assisted buildContext: BuildContext,
|
@Assisted buildContext: BuildContext,
|
||||||
@Assisted plugins: List<Plugin>,
|
@Assisted plugins: List<Plugin>,
|
||||||
|
|
@ -87,7 +83,7 @@ class FtueFlowNode(
|
||||||
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||||
return when (navTarget) {
|
return when (navTarget) {
|
||||||
NavTarget.Placeholder -> {
|
NavTarget.Placeholder -> {
|
||||||
createNode<PlaceholderNode>(buildContext)
|
emptyNode(buildContext)
|
||||||
}
|
}
|
||||||
is NavTarget.SessionVerification -> {
|
is NavTarget.SessionVerification -> {
|
||||||
val callback = object : FtueSessionVerificationFlowNode.Callback {
|
val callback = object : FtueSessionVerificationFlowNode.Callback {
|
||||||
|
|
@ -146,17 +142,3 @@ class FtueFlowNode(
|
||||||
BackstackView()
|
BackstackView()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ContributesNode(AppScope::class)
|
|
||||||
@Inject
|
|
||||||
class PlaceholderNode(
|
|
||||||
@Assisted buildContext: BuildContext,
|
|
||||||
@Assisted plugins: List<Plugin>,
|
|
||||||
) : Node(buildContext, plugins = plugins) {
|
|
||||||
@Composable
|
|
||||||
override fun View(modifier: Modifier) {
|
|
||||||
Box(modifier = modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
|
||||||
CircularProgressIndicator()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -14,13 +14,13 @@ import com.bumble.appyx.core.node.Node
|
||||||
import com.bumble.appyx.core.plugin.Plugin
|
import com.bumble.appyx.core.plugin.Plugin
|
||||||
import dev.zacsweers.metro.AppScope
|
import dev.zacsweers.metro.AppScope
|
||||||
import dev.zacsweers.metro.Assisted
|
import dev.zacsweers.metro.Assisted
|
||||||
import dev.zacsweers.metro.Inject
|
import dev.zacsweers.metro.AssistedInject
|
||||||
import io.element.android.annotations.ContributesNode
|
import io.element.android.annotations.ContributesNode
|
||||||
import io.element.android.libraries.architecture.NodeInputs
|
import io.element.android.libraries.architecture.NodeInputs
|
||||||
import io.element.android.libraries.architecture.inputs
|
import io.element.android.libraries.architecture.inputs
|
||||||
|
|
||||||
@ContributesNode(AppScope::class)
|
@ContributesNode(AppScope::class)
|
||||||
@Inject
|
@AssistedInject
|
||||||
class NotificationsOptInNode(
|
class NotificationsOptInNode(
|
||||||
@Assisted buildContext: BuildContext,
|
@Assisted buildContext: BuildContext,
|
||||||
@Assisted plugins: List<Plugin>,
|
@Assisted plugins: List<Plugin>,
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import dev.zacsweers.metro.Assisted
|
import dev.zacsweers.metro.Assisted
|
||||||
import dev.zacsweers.metro.AssistedFactory
|
import dev.zacsweers.metro.AssistedFactory
|
||||||
import dev.zacsweers.metro.Inject
|
import dev.zacsweers.metro.AssistedInject
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.di.annotations.AppCoroutineScope
|
import io.element.android.libraries.di.annotations.AppCoroutineScope
|
||||||
import io.element.android.libraries.permissions.api.PermissionStateProvider
|
import io.element.android.libraries.permissions.api.PermissionStateProvider
|
||||||
|
|
@ -25,7 +25,7 @@ import io.element.android.services.toolbox.api.sdk.BuildVersionSdkIntProvider
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@Inject
|
@AssistedInject
|
||||||
class NotificationsOptInPresenter(
|
class NotificationsOptInPresenter(
|
||||||
permissionsPresenterFactory: PermissionsPresenter.Factory,
|
permissionsPresenterFactory: PermissionsPresenter.Factory,
|
||||||
@Assisted private val callback: NotificationsOptInNode.Callback,
|
@Assisted private val callback: NotificationsOptInNode.Callback,
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ import com.bumble.appyx.navmodel.backstack.operation.newRoot
|
||||||
import com.bumble.appyx.navmodel.backstack.operation.pop
|
import com.bumble.appyx.navmodel.backstack.operation.pop
|
||||||
import com.bumble.appyx.navmodel.backstack.operation.push
|
import com.bumble.appyx.navmodel.backstack.operation.push
|
||||||
import dev.zacsweers.metro.Assisted
|
import dev.zacsweers.metro.Assisted
|
||||||
import dev.zacsweers.metro.Inject
|
import dev.zacsweers.metro.AssistedInject
|
||||||
import io.element.android.annotations.ContributesNode
|
import io.element.android.annotations.ContributesNode
|
||||||
import io.element.android.appconfig.LearnMoreConfig
|
import io.element.android.appconfig.LearnMoreConfig
|
||||||
import io.element.android.features.ftue.impl.sessionverification.choosemode.ChooseSelfVerificationModeNode
|
import io.element.android.features.ftue.impl.sessionverification.choosemode.ChooseSelfVerificationModeNode
|
||||||
|
|
@ -37,7 +37,7 @@ import kotlinx.coroutines.launch
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
@ContributesNode(SessionScope::class)
|
@ContributesNode(SessionScope::class)
|
||||||
@Inject
|
@AssistedInject
|
||||||
class FtueSessionVerificationFlowNode(
|
class FtueSessionVerificationFlowNode(
|
||||||
@Assisted buildContext: BuildContext,
|
@Assisted buildContext: BuildContext,
|
||||||
@Assisted plugins: List<Plugin>,
|
@Assisted plugins: List<Plugin>,
|
||||||
|
|
|
||||||
|
|
@ -14,14 +14,14 @@ import com.bumble.appyx.core.node.Node
|
||||||
import com.bumble.appyx.core.plugin.Plugin
|
import com.bumble.appyx.core.plugin.Plugin
|
||||||
import com.bumble.appyx.core.plugin.plugins
|
import com.bumble.appyx.core.plugin.plugins
|
||||||
import dev.zacsweers.metro.Assisted
|
import dev.zacsweers.metro.Assisted
|
||||||
import dev.zacsweers.metro.Inject
|
import dev.zacsweers.metro.AssistedInject
|
||||||
import io.element.android.annotations.ContributesNode
|
import io.element.android.annotations.ContributesNode
|
||||||
import io.element.android.features.logout.api.direct.DirectLogoutView
|
import io.element.android.features.logout.api.direct.DirectLogoutView
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.di.SessionScope
|
import io.element.android.libraries.di.SessionScope
|
||||||
|
|
||||||
@ContributesNode(SessionScope::class)
|
@ContributesNode(SessionScope::class)
|
||||||
@Inject
|
@AssistedInject
|
||||||
class ChooseSelfVerificationModeNode(
|
class ChooseSelfVerificationModeNode(
|
||||||
@Assisted buildContext: BuildContext,
|
@Assisted buildContext: BuildContext,
|
||||||
@Assisted plugins: List<Plugin>,
|
@Assisted plugins: List<Plugin>,
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ class ChooseSelfVerificationModePresenter(
|
||||||
) : Presenter<ChooseSelfVerificationModeState> {
|
) : Presenter<ChooseSelfVerificationModeState> {
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): ChooseSelfVerificationModeState {
|
override fun present(): ChooseSelfVerificationModeState {
|
||||||
val isLastDevice by encryptionService.isLastDevice.collectAsState()
|
val hasDevicesToVerifyAgainst by encryptionService.hasDevicesToVerifyAgainst.collectAsState()
|
||||||
val recoveryState by encryptionService.recoveryStateStateFlow.collectAsState()
|
val recoveryState by encryptionService.recoveryStateStateFlow.collectAsState()
|
||||||
val canEnterRecoveryKey by remember { derivedStateOf { recoveryState == RecoveryState.INCOMPLETE } }
|
val canEnterRecoveryKey by remember { derivedStateOf { recoveryState == RecoveryState.INCOMPLETE } }
|
||||||
|
|
||||||
|
|
@ -39,7 +39,7 @@ class ChooseSelfVerificationModePresenter(
|
||||||
}
|
}
|
||||||
|
|
||||||
return ChooseSelfVerificationModeState(
|
return ChooseSelfVerificationModeState(
|
||||||
isLastDevice = isLastDevice,
|
canUseAnotherDevice = hasDevicesToVerifyAgainst,
|
||||||
canEnterRecoveryKey = canEnterRecoveryKey,
|
canEnterRecoveryKey = canEnterRecoveryKey,
|
||||||
directLogoutState = directLogoutState,
|
directLogoutState = directLogoutState,
|
||||||
eventSink = ::eventHandler,
|
eventSink = ::eventHandler,
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ package io.element.android.features.ftue.impl.sessionverification.choosemode
|
||||||
import io.element.android.features.logout.api.direct.DirectLogoutState
|
import io.element.android.features.logout.api.direct.DirectLogoutState
|
||||||
|
|
||||||
data class ChooseSelfVerificationModeState(
|
data class ChooseSelfVerificationModeState(
|
||||||
val isLastDevice: Boolean,
|
val canUseAnotherDevice: Boolean,
|
||||||
val canEnterRecoveryKey: Boolean,
|
val canEnterRecoveryKey: Boolean,
|
||||||
val directLogoutState: DirectLogoutState,
|
val directLogoutState: DirectLogoutState,
|
||||||
val eventSink: (ChooseSelfVerificationModeEvent) -> Unit,
|
val eventSink: (ChooseSelfVerificationModeEvent) -> Unit,
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue