Merge branch 'develop' into feature/fga/role_and_permissions_rework
This commit is contained in:
commit
79cf41d59c
185 changed files with 1900 additions and 1125 deletions
4
.github/workflows/build.yml
vendored
4
.github/workflows/build.yml
vendored
|
|
@ -53,7 +53,7 @@ jobs:
|
||||||
run: ./gradlew :app:assembleGplayDebug app:assembleFDroidDebug -PallWarningsAsErrors=true $CI_GRADLE_ARG_PROPERTIES
|
run: ./gradlew :app:assembleGplayDebug app:assembleFDroidDebug -PallWarningsAsErrors=true $CI_GRADLE_ARG_PROPERTIES
|
||||||
- name: Upload debug APKs
|
- name: Upload debug APKs
|
||||||
if: ${{ matrix.variant == 'debug' }}
|
if: ${{ matrix.variant == 'debug' }}
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: elementx-debug
|
name: elementx-debug
|
||||||
path: |
|
path: |
|
||||||
|
|
@ -61,7 +61,7 @@ jobs:
|
||||||
app/build/outputs/apk/fdroid/debug/*-universal-debug.apk
|
app/build/outputs/apk/fdroid/debug/*-universal-debug.apk
|
||||||
- name: Upload x86_64 APK for Maestro
|
- name: Upload x86_64 APK for Maestro
|
||||||
if: ${{ matrix.variant == 'debug' }}
|
if: ${{ matrix.variant == 'debug' }}
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: elementx-apk-maestro
|
name: elementx-apk-maestro
|
||||||
path: |
|
path: |
|
||||||
|
|
|
||||||
2
.github/workflows/build_enterprise.yml
vendored
2
.github/workflows/build_enterprise.yml
vendored
|
|
@ -61,7 +61,7 @@ jobs:
|
||||||
run: ./gradlew :app:assembleGplayDebug -PallWarningsAsErrors=true $CI_GRADLE_ARG_PROPERTIES
|
run: ./gradlew :app:assembleGplayDebug -PallWarningsAsErrors=true $CI_GRADLE_ARG_PROPERTIES
|
||||||
- name: Upload debug Enterprise APKs
|
- name: Upload debug Enterprise APKs
|
||||||
if: ${{ matrix.variant == 'debug' }}
|
if: ${{ matrix.variant == 'debug' }}
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: elementx-enterprise-debug
|
name: elementx-enterprise-debug
|
||||||
path: |
|
path: |
|
||||||
|
|
|
||||||
6
.github/workflows/maestro-local.yml
vendored
6
.github/workflows/maestro-local.yml
vendored
|
|
@ -44,7 +44,7 @@ jobs:
|
||||||
ELEMENT_ANDROID_MAPTILER_LIGHT_MAP_ID: ${{ secrets.MAPTILER_LIGHT_MAP_ID }}
|
ELEMENT_ANDROID_MAPTILER_LIGHT_MAP_ID: ${{ secrets.MAPTILER_LIGHT_MAP_ID }}
|
||||||
ELEMENT_ANDROID_MAPTILER_DARK_MAP_ID: ${{ secrets.MAPTILER_DARK_MAP_ID }}
|
ELEMENT_ANDROID_MAPTILER_DARK_MAP_ID: ${{ secrets.MAPTILER_DARK_MAP_ID }}
|
||||||
- name: Upload APK as artifact
|
- name: Upload APK as artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: elementx-apk-maestro
|
name: elementx-apk-maestro
|
||||||
path: |
|
path: |
|
||||||
|
|
@ -69,7 +69,7 @@ jobs:
|
||||||
# https://github.com/actions/checkout/issues/881
|
# https://github.com/actions/checkout/issues/881
|
||||||
ref: ${{ github.ref }}
|
ref: ${{ github.ref }}
|
||||||
- name: Download APK artifact from previous job
|
- name: Download APK artifact from previous job
|
||||||
uses: actions/download-artifact@v5
|
uses: actions/download-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: elementx-apk-maestro
|
name: elementx-apk-maestro
|
||||||
- name: Enable KVM group perms
|
- name: Enable KVM group perms
|
||||||
|
|
@ -102,7 +102,7 @@ jobs:
|
||||||
script: |
|
script: |
|
||||||
.github/workflows/scripts/maestro/maestro-local-with-screen-recording.sh app-gplay-x86_64-debug.apk
|
.github/workflows/scripts/maestro/maestro-local-with-screen-recording.sh app-gplay-x86_64-debug.apk
|
||||||
- name: Upload test results
|
- name: Upload test results
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: test-results
|
name: test-results
|
||||||
path: |
|
path: |
|
||||||
|
|
|
||||||
4
.github/workflows/nightlyReports.yml
vendored
4
.github/workflows/nightlyReports.yml
vendored
|
|
@ -42,7 +42,7 @@ jobs:
|
||||||
|
|
||||||
- name: ✅ Upload kover report
|
- name: ✅ Upload kover report
|
||||||
if: always()
|
if: always()
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: kover-results
|
name: kover-results
|
||||||
path: |
|
path: |
|
||||||
|
|
@ -74,7 +74,7 @@ jobs:
|
||||||
run: ./gradlew dependencyCheckAnalyze $CI_GRADLE_ARG_PROPERTIES
|
run: ./gradlew dependencyCheckAnalyze $CI_GRADLE_ARG_PROPERTIES
|
||||||
- name: Upload dependency analysis
|
- name: Upload dependency analysis
|
||||||
if: always()
|
if: always()
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: dependency-analysis
|
name: dependency-analysis
|
||||||
path: build/reports/dependency-check-report.html
|
path: build/reports/dependency-check-report.html
|
||||||
|
|
|
||||||
10
.github/workflows/quality.yml
vendored
10
.github/workflows/quality.yml
vendored
|
|
@ -97,7 +97,7 @@ jobs:
|
||||||
run: ./gradlew :tests:konsist:testDebugUnitTest $CI_GRADLE_ARG_PROPERTIES --no-daemon
|
run: ./gradlew :tests:konsist:testDebugUnitTest $CI_GRADLE_ARG_PROPERTIES --no-daemon
|
||||||
- name: Upload reports
|
- name: Upload reports
|
||||||
if: always()
|
if: always()
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: konsist-report
|
name: konsist-report
|
||||||
path: |
|
path: |
|
||||||
|
|
@ -174,7 +174,7 @@ jobs:
|
||||||
run: ./gradlew :app:lintGplayDebug :app:lintFdroidDebug lintDebug $CI_GRADLE_ARG_PROPERTIES --continue
|
run: ./gradlew :app:lintGplayDebug :app:lintFdroidDebug lintDebug $CI_GRADLE_ARG_PROPERTIES --continue
|
||||||
- name: Upload reports
|
- name: Upload reports
|
||||||
if: always()
|
if: always()
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: linting-report
|
name: linting-report
|
||||||
path: |
|
path: |
|
||||||
|
|
@ -214,7 +214,7 @@ jobs:
|
||||||
run: ./gradlew detekt $CI_GRADLE_ARG_PROPERTIES --no-daemon
|
run: ./gradlew detekt $CI_GRADLE_ARG_PROPERTIES --no-daemon
|
||||||
- name: Upload reports
|
- name: Upload reports
|
||||||
if: always()
|
if: always()
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: detekt-report
|
name: detekt-report
|
||||||
path: |
|
path: |
|
||||||
|
|
@ -254,7 +254,7 @@ jobs:
|
||||||
run: ./gradlew ktlintCheck $CI_GRADLE_ARG_PROPERTIES
|
run: ./gradlew ktlintCheck $CI_GRADLE_ARG_PROPERTIES
|
||||||
- name: Upload reports
|
- name: Upload reports
|
||||||
if: always()
|
if: always()
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: ktlint-report
|
name: ktlint-report
|
||||||
path: |
|
path: |
|
||||||
|
|
@ -317,7 +317,7 @@ jobs:
|
||||||
# https://github.com/actions/checkout/issues/881
|
# https://github.com/actions/checkout/issues/881
|
||||||
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }}
|
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }}
|
||||||
- name: Download reports from previous jobs
|
- name: Download reports from previous jobs
|
||||||
uses: actions/download-artifact@v5
|
uses: actions/download-artifact@v6
|
||||||
- name: Prepare Danger
|
- name: Prepare Danger
|
||||||
if: always()
|
if: always()
|
||||||
run: |
|
run: |
|
||||||
|
|
|
||||||
6
.github/workflows/release.yml
vendored
6
.github/workflows/release.yml
vendored
|
|
@ -38,7 +38,7 @@ jobs:
|
||||||
ELEMENT_CALL_RAGESHAKE_URL: ${{ secrets.ELEMENT_CALL_RAGESHAKE_URL }}
|
ELEMENT_CALL_RAGESHAKE_URL: ${{ secrets.ELEMENT_CALL_RAGESHAKE_URL }}
|
||||||
run: ./gradlew bundleGplayRelease $CI_GRADLE_ARG_PROPERTIES
|
run: ./gradlew bundleGplayRelease $CI_GRADLE_ARG_PROPERTIES
|
||||||
- name: Upload bundle as artifact
|
- name: Upload bundle as artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: elementx-app-gplay-bundle-unsigned
|
name: elementx-app-gplay-bundle-unsigned
|
||||||
path: |
|
path: |
|
||||||
|
|
@ -74,7 +74,7 @@ jobs:
|
||||||
ELEMENT_ANDROID_MAPTILER_DARK_MAP_ID: ${{ secrets.MAPTILER_DARK_MAP_ID }}
|
ELEMENT_ANDROID_MAPTILER_DARK_MAP_ID: ${{ secrets.MAPTILER_DARK_MAP_ID }}
|
||||||
run: ./gradlew bundleGplayRelease $CI_GRADLE_ARG_PROPERTIES
|
run: ./gradlew bundleGplayRelease $CI_GRADLE_ARG_PROPERTIES
|
||||||
- name: Upload bundle as artifact
|
- name: Upload bundle as artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: elementx-enterprise-app-gplay-bundle-unsigned
|
name: elementx-enterprise-app-gplay-bundle-unsigned
|
||||||
path: |
|
path: |
|
||||||
|
|
@ -102,7 +102,7 @@ jobs:
|
||||||
ELEMENT_ANDROID_MAPTILER_DARK_MAP_ID: ${{ secrets.MAPTILER_DARK_MAP_ID }}
|
ELEMENT_ANDROID_MAPTILER_DARK_MAP_ID: ${{ secrets.MAPTILER_DARK_MAP_ID }}
|
||||||
run: ./gradlew assembleFdroidRelease $CI_GRADLE_ARG_PROPERTIES
|
run: ./gradlew assembleFdroidRelease $CI_GRADLE_ARG_PROPERTIES
|
||||||
- name: Upload apks as artifact
|
- name: Upload apks as artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: elementx-app-fdroid-apks-unsigned
|
name: elementx-app-fdroid-apks-unsigned
|
||||||
path: |
|
path: |
|
||||||
|
|
|
||||||
4
.github/workflows/tests.yml
vendored
4
.github/workflows/tests.yml
vendored
|
|
@ -61,7 +61,7 @@ jobs:
|
||||||
|
|
||||||
- name: 🚫 Upload kover failed coverage reports
|
- name: 🚫 Upload kover failed coverage reports
|
||||||
if: failure()
|
if: failure()
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: kover-error-report
|
name: kover-error-report
|
||||||
path: |
|
path: |
|
||||||
|
|
@ -73,7 +73,7 @@ jobs:
|
||||||
|
|
||||||
- name: 🚫 Upload test results on error
|
- name: 🚫 Upload test results on error
|
||||||
if: failure()
|
if: failure()
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: tests-and-screenshot-tests-results
|
name: tests-and-screenshot-tests-results
|
||||||
path: |
|
path: |
|
||||||
|
|
|
||||||
83
CHANGES.md
83
CHANGES.md
|
|
@ -1,3 +1,86 @@
|
||||||
|
Changes in Element X v25.11.2
|
||||||
|
=============================
|
||||||
|
|
||||||
|
<!-- Release notes generated using configuration in .github/release.yml at v25.11.2 -->
|
||||||
|
|
||||||
|
## What's Changed
|
||||||
|
### ✨ Features
|
||||||
|
* Enable access to security and privacy by @bmarty in https://github.com/element-hq/element-x-android/pull/5566
|
||||||
|
* Add ability to forward a media from the media viewer and the gallery by @bmarty in https://github.com/element-hq/element-x-android/pull/5622
|
||||||
|
* Split notifications for messages in threads by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5595
|
||||||
|
### 🙌 Improvements
|
||||||
|
* Enable `SyncNotificationsWithWorkManager` in nightly and debug builds by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5573
|
||||||
|
* Confirm exit without saving change in room details edit screen by @bmarty in https://github.com/element-hq/element-x-android/pull/5618
|
||||||
|
* Space : add view members entry by @ganfra in https://github.com/element-hq/element-x-android/pull/5619
|
||||||
|
* Update notification sound by @bmarty in https://github.com/element-hq/element-x-android/pull/5667
|
||||||
|
* Use the new notification sound only on debug and nightly build by @bmarty in https://github.com/element-hq/element-x-android/pull/5673
|
||||||
|
* Make sure we know the session verification state before showing the options to verify the session by @bmarty in https://github.com/element-hq/element-x-android/pull/5677
|
||||||
|
### 🐛 Bugfixes
|
||||||
|
* Improve how brand color is applied. by @bmarty in https://github.com/element-hq/element-x-android/pull/5584
|
||||||
|
* Improve wellknown retrieval API by @bmarty in https://github.com/element-hq/element-x-android/pull/5587
|
||||||
|
* Clearing the room list search clears the search term too by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5603
|
||||||
|
* Delete pin code only when the last session is deleted by @bmarty in https://github.com/element-hq/element-x-android/pull/5600
|
||||||
|
* Fix issues with WorkManager on Android 12 and below by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5606
|
||||||
|
* Fix marking a room as read re-instantiates its timeline by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5628
|
||||||
|
* Display only valid emojis in recent emoji list by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5612
|
||||||
|
* Fix navigation issue. by @bmarty in https://github.com/element-hq/element-x-android/pull/5666
|
||||||
|
* Fix forward events from media viewer from pinned media timeline by @bmarty in https://github.com/element-hq/element-x-android/pull/5669
|
||||||
|
* Try fixing 'Timeline Event object has already been destroyed' by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5675
|
||||||
|
* Use the SDK Client to check whether a homeserver is compatible by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5664
|
||||||
|
### 🗣 Translations
|
||||||
|
* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/5610
|
||||||
|
* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/5662
|
||||||
|
### 🧱 Build
|
||||||
|
* Remove `@Inject`, not necessary anymore when class is annotated with `@ContributesBinding` by @bmarty in https://github.com/element-hq/element-x-android/pull/5589
|
||||||
|
* Upgrade ktlint to 1.7.1 and ensure Renovate will upgrade the version by @bmarty in https://github.com/element-hq/element-x-android/pull/5638
|
||||||
|
* Improve architecture around Nodes by @bmarty in https://github.com/element-hq/element-x-android/pull/5641
|
||||||
|
* Move dependencies block out of the android block. by @bmarty in https://github.com/element-hq/element-x-android/pull/5674
|
||||||
|
* Always use the handleEvent(s) function the same way. by @bmarty in https://github.com/element-hq/element-x-android/pull/5672
|
||||||
|
### Dependency upgrades
|
||||||
|
* fix(deps): update metro to v0.7.2 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5576
|
||||||
|
* fix(deps): update dependencyanalysis to v3.2.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5577
|
||||||
|
* fix(deps): update dependency io.sentry:sentry-android to v8.24.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5586
|
||||||
|
* fix(deps): update dependency androidx.work:work-runtime-ktx to v2.11.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5590
|
||||||
|
* fix(deps): update dependency com.posthog:posthog-android to v3.25.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5594
|
||||||
|
* fix(deps): update dependency com.google.crypto.tink:tink-android to v1.19.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5572
|
||||||
|
* Update plugin sonarqube to v7.0.1.6134 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5605
|
||||||
|
* fix(deps): update dependency org.matrix.rustcomponents:sdk-android to v25.10.28 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5620
|
||||||
|
* fix(deps): update dependencyanalysis to v3.3.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5602
|
||||||
|
* fix(deps): update dependency com.github.matrix-org:matrix-analytics-events to v0.29.2 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5621
|
||||||
|
* fix(deps): update dependencyanalysis to v3.4.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5624
|
||||||
|
* fix(deps): update dependency org.matrix.rustcomponents:sdk-android to v25.10.29 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5625
|
||||||
|
* fix(deps): update dependency io.sentry:sentry-android to v8.25.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5629
|
||||||
|
* fix(deps): update dependencyanalysis to v3.4.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5642
|
||||||
|
* fix(deps): update dependency com.squareup.okhttp3:okhttp-bom to v5.3.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5644
|
||||||
|
* chore(deps): update danger/danger-js action to v13.0.5 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5652
|
||||||
|
* fix(deps): update dependency com.google.firebase:firebase-bom to v34.5.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5643
|
||||||
|
* fix(deps): update firebaseappdistribution to v5.2.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5640
|
||||||
|
* fix(deps): update metro to v0.7.3 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5663
|
||||||
|
* fix(deps): update dependency org.matrix.rustcomponents:sdk-android to v25.10.31 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5657
|
||||||
|
* Update GitHub Artifact Actions (major) by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5609
|
||||||
|
* Update dependency io.element.android:element-call-embedded to v0.16.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5598
|
||||||
|
* Update roborazzi to v1.51.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5676
|
||||||
|
* fix(deps): update dependency org.matrix.rustcomponents:sdk-android to v25.11.4 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5681
|
||||||
|
* fix(deps): update metro to v0.7.4 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5683
|
||||||
|
### Others
|
||||||
|
* Improve code around Element .well-known configuration by @bmarty in https://github.com/element-hq/element-x-android/pull/5565
|
||||||
|
* misc: display offline banner for all LoggedIn screens by @ganfra in https://github.com/element-hq/element-x-android/pull/5574
|
||||||
|
* Remove icon preview duplicate by @bmarty in https://github.com/element-hq/element-x-android/pull/5588
|
||||||
|
* Remove application navigation state usage in the push module by @bmarty in https://github.com/element-hq/element-x-android/pull/5596
|
||||||
|
* Design : update Home TopBar and RoomList Filters by @ganfra in https://github.com/element-hq/element-x-android/pull/5599
|
||||||
|
* Add missing tests on the analytic modules by @bmarty in https://github.com/element-hq/element-x-android/pull/5604
|
||||||
|
* design(space): let SpaceRoomItemView divider be full width by @ganfra in https://github.com/element-hq/element-x-android/pull/5597
|
||||||
|
* Update notification style by @bmarty in https://github.com/element-hq/element-x-android/pull/5607
|
||||||
|
* Improve how data is handled for the WorkManager. by @bmarty in https://github.com/element-hq/element-x-android/pull/5592
|
||||||
|
* Revert "Make sure declining a call stops observing the ringing call state" by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5615
|
||||||
|
* Misc : space flow inject room by @ganfra in https://github.com/element-hq/element-x-android/pull/5614
|
||||||
|
* Enable `SyncNotificationsWithWorkManager` by default in release mode apps too by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5646
|
||||||
|
* Revert "Update notification sound" by @bmarty in https://github.com/element-hq/element-x-android/pull/5671
|
||||||
|
* Introduce new query to count accounts by @bmarty in https://github.com/element-hq/element-x-android/pull/5678
|
||||||
|
|
||||||
|
|
||||||
|
**Full Changelog**: https://github.com/element-hq/element-x-android/compare/v25.11.0...v25.11.2
|
||||||
|
|
||||||
Changes in Element X v25.11.0
|
Changes in Element X v25.11.0
|
||||||
=============================
|
=============================
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,13 +9,14 @@ package io.element.android.x.di
|
||||||
|
|
||||||
import dev.zacsweers.metro.GraphExtension
|
import dev.zacsweers.metro.GraphExtension
|
||||||
import dev.zacsweers.metro.Provides
|
import dev.zacsweers.metro.Provides
|
||||||
|
import io.element.android.appnav.di.TimelineBindings
|
||||||
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.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, TimelineBindings {
|
||||||
@GraphExtension.Factory
|
@GraphExtension.Factory
|
||||||
interface Factory {
|
interface Factory {
|
||||||
fun create(
|
fun create(
|
||||||
|
|
|
||||||
|
|
@ -349,7 +349,7 @@ class RootFlowNode(
|
||||||
} else {
|
} else {
|
||||||
// wait for the current session to be restored
|
// wait for the current session to be restored
|
||||||
val loggedInFlowNode = attachSession(latestSessionId)
|
val loggedInFlowNode = attachSession(latestSessionId)
|
||||||
if (sessionStore.getAllSessions().size > 1) {
|
if (sessionStore.numberOfSessions() > 1) {
|
||||||
// Several accounts, let the user choose which one to use
|
// Several accounts, let the user choose which one to use
|
||||||
backstack.push(
|
backstack.push(
|
||||||
NavTarget.AccountSelect(
|
NavTarget.AccountSelect(
|
||||||
|
|
@ -379,7 +379,7 @@ class RootFlowNode(
|
||||||
is PermalinkData.FallbackLink -> Unit
|
is PermalinkData.FallbackLink -> Unit
|
||||||
is PermalinkData.RoomEmailInviteLink -> Unit
|
is PermalinkData.RoomEmailInviteLink -> Unit
|
||||||
else -> {
|
else -> {
|
||||||
if (sessionStore.getAllSessions().size > 1) {
|
if (sessionStore.numberOfSessions() > 1) {
|
||||||
// Several accounts, let the user choose which one to use
|
// Several accounts, let the user choose which one to use
|
||||||
backstack.push(
|
backstack.push(
|
||||||
NavTarget.AccountSelect(
|
NavTarget.AccountSelect(
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
/*
|
||||||
|
* 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.appnav.di
|
||||||
|
|
||||||
|
import io.element.android.features.messages.api.pinned.PinnedEventsTimelineProvider
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.TimelineProvider
|
||||||
|
|
||||||
|
interface TimelineBindings {
|
||||||
|
val timelineProvider: TimelineProvider
|
||||||
|
val pinnedEventsTimelineProvider: PinnedEventsTimelineProvider
|
||||||
|
}
|
||||||
|
|
@ -30,7 +30,6 @@ import io.element.android.features.joinroom.api.JoinRoomEntryPoint
|
||||||
import io.element.android.features.roomaliasesolver.api.RoomAliasResolverEntryPoint
|
import io.element.android.features.roomaliasesolver.api.RoomAliasResolverEntryPoint
|
||||||
import io.element.android.features.roomaliasesolver.api.RoomAliasResolverEntryPoint.Params
|
import io.element.android.features.roomaliasesolver.api.RoomAliasResolverEntryPoint.Params
|
||||||
import io.element.android.features.roomdirectory.api.RoomDescription
|
import io.element.android.features.roomdirectory.api.RoomDescription
|
||||||
import io.element.android.features.space.api.SpaceEntryPoint
|
|
||||||
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.NodeInputs
|
import io.element.android.libraries.architecture.NodeInputs
|
||||||
|
|
@ -70,7 +69,6 @@ class RoomFlowNode(
|
||||||
private val joinRoomEntryPoint: JoinRoomEntryPoint,
|
private val joinRoomEntryPoint: JoinRoomEntryPoint,
|
||||||
private val roomAliasResolverEntryPoint: RoomAliasResolverEntryPoint,
|
private val roomAliasResolverEntryPoint: RoomAliasResolverEntryPoint,
|
||||||
private val membershipObserver: RoomMembershipObserver,
|
private val membershipObserver: RoomMembershipObserver,
|
||||||
private val spaceEntryPoint: SpaceEntryPoint,
|
|
||||||
) : BaseFlowNode<RoomFlowNode.NavTarget>(
|
) : BaseFlowNode<RoomFlowNode.NavTarget>(
|
||||||
backstack = BackStack(
|
backstack = BackStack(
|
||||||
initialElement = NavTarget.Loading,
|
initialElement = NavTarget.Loading,
|
||||||
|
|
@ -105,9 +103,6 @@ class RoomFlowNode(
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class JoinedRoom(val roomId: RoomId) : NavTarget
|
data class JoinedRoom(val roomId: RoomId) : NavTarget
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class JoinedSpace(val spaceId: RoomId) : NavTarget
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBuilt() {
|
override fun onBuilt() {
|
||||||
|
|
@ -209,15 +204,6 @@ class RoomFlowNode(
|
||||||
)
|
)
|
||||||
createNode<JoinedRoomFlowNode>(buildContext, plugins = listOf(inputs) + roomFlowNodeCallback)
|
createNode<JoinedRoomFlowNode>(buildContext, plugins = listOf(inputs) + roomFlowNodeCallback)
|
||||||
}
|
}
|
||||||
is NavTarget.JoinedSpace -> {
|
|
||||||
val spaceCallback = plugins<SpaceEntryPoint.Callback>().single()
|
|
||||||
spaceEntryPoint.createNode(
|
|
||||||
parentNode = this,
|
|
||||||
buildContext = buildContext,
|
|
||||||
inputs = SpaceEntryPoint.Inputs(roomId = navTarget.spaceId),
|
|
||||||
callback = spaceCallback,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,10 +22,10 @@ import dev.zacsweers.metro.Assisted
|
||||||
import dev.zacsweers.metro.AssistedInject
|
import dev.zacsweers.metro.AssistedInject
|
||||||
import io.element.android.annotations.ContributesNode
|
import io.element.android.annotations.ContributesNode
|
||||||
import io.element.android.appnav.di.RoomGraphFactory
|
import io.element.android.appnav.di.RoomGraphFactory
|
||||||
|
import io.element.android.appnav.di.TimelineBindings
|
||||||
import io.element.android.appnav.room.RoomNavigationTarget
|
import io.element.android.appnav.room.RoomNavigationTarget
|
||||||
import io.element.android.features.forward.api.ForwardEntryPoint
|
import io.element.android.features.forward.api.ForwardEntryPoint
|
||||||
import io.element.android.features.messages.api.MessagesEntryPoint
|
import io.element.android.features.messages.api.MessagesEntryPoint
|
||||||
import io.element.android.features.messages.api.MessagesEntryPointNode
|
|
||||||
import io.element.android.features.roomdetails.api.RoomDetailsEntryPoint
|
import io.element.android.features.roomdetails.api.RoomDetailsEntryPoint
|
||||||
import io.element.android.features.space.api.SpaceEntryPoint
|
import io.element.android.features.space.api.SpaceEntryPoint
|
||||||
import io.element.android.libraries.architecture.BackstackView
|
import io.element.android.libraries.architecture.BackstackView
|
||||||
|
|
@ -47,8 +47,6 @@ import io.element.android.libraries.matrix.api.room.JoinedRoom
|
||||||
import io.element.android.services.appnavstate.api.ActiveRoomsHolder
|
import io.element.android.services.appnavstate.api.ActiveRoomsHolder
|
||||||
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.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
@ -136,8 +134,8 @@ class JoinedRoomLoadedFlowNode(
|
||||||
callback.handlePermalinkClick(data, pushToBackstack)
|
callback.handlePermalinkClick(data, pushToBackstack)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun startForwardEventFlow(eventId: EventId) {
|
override fun startForwardEventFlow(eventId: EventId, fromPinnedEvents: Boolean) {
|
||||||
backstack.push(NavTarget.ForwardEvent(eventId))
|
backstack.push(NavTarget.ForwardEvent(eventId, fromPinnedEvents))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return roomDetailsEntryPoint.createNode(
|
return roomDetailsEntryPoint.createNode(
|
||||||
|
|
@ -169,7 +167,11 @@ class JoinedRoomLoadedFlowNode(
|
||||||
createSpaceNode(buildContext)
|
createSpaceNode(buildContext)
|
||||||
}
|
}
|
||||||
is NavTarget.ForwardEvent -> {
|
is NavTarget.ForwardEvent -> {
|
||||||
val timelineProvider = { MutableStateFlow(inputs.room.liveTimeline).asStateFlow() }
|
val timelineProvider = if (navTarget.fromPinnedEvents) {
|
||||||
|
(graph as TimelineBindings).pinnedEventsTimelineProvider
|
||||||
|
} else {
|
||||||
|
(graph as TimelineBindings).timelineProvider
|
||||||
|
}
|
||||||
val params = ForwardEntryPoint.Params(navTarget.eventId, timelineProvider)
|
val params = ForwardEntryPoint.Params(navTarget.eventId, timelineProvider)
|
||||||
val callback = object : ForwardEntryPoint.Callback {
|
val callback = object : ForwardEntryPoint.Callback {
|
||||||
override fun onDone(roomIds: List<RoomId>) {
|
override fun onDone(roomIds: List<RoomId>) {
|
||||||
|
|
@ -195,10 +197,6 @@ class JoinedRoomLoadedFlowNode(
|
||||||
callback.navigateToRoom(roomId, viaParameters)
|
callback.navigateToRoom(roomId, viaParameters)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun navigateToRoomDetails() {
|
|
||||||
backstack.push(NavTarget.RoomDetails)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun navigateToRoomMemberList() {
|
override fun navigateToRoomMemberList() {
|
||||||
backstack.push(NavTarget.RoomMemberList)
|
backstack.push(NavTarget.RoomMemberList)
|
||||||
}
|
}
|
||||||
|
|
@ -228,8 +226,8 @@ class JoinedRoomLoadedFlowNode(
|
||||||
callback.handlePermalinkClick(data, pushToBackstack)
|
callback.handlePermalinkClick(data, pushToBackstack)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun forwardEvent(eventId: EventId) {
|
override fun forwardEvent(eventId: EventId, fromPinnedEvents: Boolean) {
|
||||||
backstack.push(NavTarget.ForwardEvent(eventId))
|
backstack.push(NavTarget.ForwardEvent(eventId, fromPinnedEvents))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun navigateToRoom(roomId: RoomId) {
|
override fun navigateToRoom(roomId: RoomId) {
|
||||||
|
|
@ -266,7 +264,7 @@ class JoinedRoomLoadedFlowNode(
|
||||||
data class RoomMemberDetails(val userId: UserId) : NavTarget
|
data class RoomMemberDetails(val userId: UserId) : NavTarget
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class ForwardEvent(val eventId: EventId) : NavTarget
|
data class ForwardEvent(val eventId: EventId, val fromPinnedEvents: Boolean) : NavTarget
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data object RoomNotificationSettings : NavTarget
|
data object RoomNotificationSettings : NavTarget
|
||||||
|
|
@ -276,7 +274,7 @@ class JoinedRoomLoadedFlowNode(
|
||||||
val messageNode = waitForChildAttached<Node, NavTarget> { navTarget ->
|
val messageNode = waitForChildAttached<Node, NavTarget> { navTarget ->
|
||||||
navTarget is NavTarget.Messages
|
navTarget is NavTarget.Messages
|
||||||
}
|
}
|
||||||
(messageNode as? MessagesEntryPointNode)?.attachThread(threadId, focusedEventId)
|
(messageNode as? MessagesEntryPoint.NodeProxy)?.attachThread(threadId, focusedEventId)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
|
||||||
2
fastlane/metadata/android/en-US/changelogs/202511020.txt
Normal file
2
fastlane/metadata/android/en-US/changelogs/202511020.txt
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
Main changes in this version: bug fixes and improvements.
|
||||||
|
Full changelog: https://github.com/element-hq/element-x-android/releases
|
||||||
|
|
@ -93,6 +93,7 @@ dependencies {
|
||||||
testImplementation(projects.libraries.featureflag.test)
|
testImplementation(projects.libraries.featureflag.test)
|
||||||
testImplementation(projects.libraries.preferences.test)
|
testImplementation(projects.libraries.preferences.test)
|
||||||
testImplementation(projects.libraries.matrix.test)
|
testImplementation(projects.libraries.matrix.test)
|
||||||
|
testImplementation(projects.libraries.matrixuiTest)
|
||||||
testImplementation(projects.libraries.push.test)
|
testImplementation(projects.libraries.push.test)
|
||||||
testImplementation(projects.services.analytics.test)
|
testImplementation(projects.services.analytics.test)
|
||||||
testImplementation(projects.services.appnavstate.test)
|
testImplementation(projects.services.appnavstate.test)
|
||||||
|
|
|
||||||
|
|
@ -201,7 +201,7 @@ class CallScreenPresenter(
|
||||||
userAgent = userAgent,
|
userAgent = userAgent,
|
||||||
isCallActive = isWidgetLoaded,
|
isCallActive = isWidgetLoaded,
|
||||||
isInWidgetMode = isInWidgetMode,
|
isInWidgetMode = isInWidgetMode,
|
||||||
eventSink = { handleEvents(it) },
|
eventSink = ::handleEvents,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||||
import io.element.android.libraries.matrix.test.A_USER_ID_2
|
import io.element.android.libraries.matrix.test.A_USER_ID_2
|
||||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||||
import io.element.android.libraries.matrix.test.FakeMatrixClientProvider
|
import io.element.android.libraries.matrix.test.FakeMatrixClientProvider
|
||||||
import io.element.android.libraries.push.test.notifications.FakeImageLoaderHolder
|
import io.element.android.libraries.matrix.ui.test.media.FakeImageLoaderHolder
|
||||||
import io.element.android.libraries.push.test.notifications.push.FakeNotificationBitmapLoader
|
import io.element.android.libraries.push.test.notifications.push.FakeNotificationBitmapLoader
|
||||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
|
|
|
||||||
|
|
@ -33,9 +33,9 @@ import io.element.android.libraries.matrix.test.FakeMatrixClientProvider
|
||||||
import io.element.android.libraries.matrix.test.room.FakeBaseRoom
|
import io.element.android.libraries.matrix.test.room.FakeBaseRoom
|
||||||
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
|
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
|
||||||
import io.element.android.libraries.matrix.test.room.aRoomInfo
|
import io.element.android.libraries.matrix.test.room.aRoomInfo
|
||||||
|
import io.element.android.libraries.matrix.ui.test.media.FakeImageLoaderHolder
|
||||||
import io.element.android.libraries.push.api.notifications.ForegroundServiceType
|
import io.element.android.libraries.push.api.notifications.ForegroundServiceType
|
||||||
import io.element.android.libraries.push.api.notifications.NotificationIdProvider
|
import io.element.android.libraries.push.api.notifications.NotificationIdProvider
|
||||||
import io.element.android.libraries.push.test.notifications.FakeImageLoaderHolder
|
|
||||||
import io.element.android.libraries.push.test.notifications.FakeOnMissedCallNotificationHandler
|
import io.element.android.libraries.push.test.notifications.FakeOnMissedCallNotificationHandler
|
||||||
import io.element.android.libraries.push.test.notifications.push.FakeNotificationBitmapLoader
|
import io.element.android.libraries.push.test.notifications.push.FakeNotificationBitmapLoader
|
||||||
import io.element.android.services.appnavstate.test.FakeAppForegroundStateService
|
import io.element.android.services.appnavstate.test.FakeAppForegroundStateService
|
||||||
|
|
@ -415,6 +415,7 @@ class DefaultActiveCallManagerTest {
|
||||||
|
|
||||||
verify { notificationManagerCompat.cancel(any()) }
|
verify { notificationManagerCompat.cancel(any()) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
@Test
|
@Test
|
||||||
fun `IncomingCall - ignore expired ring lifetime`() = runTest {
|
fun `IncomingCall - ignore expired ring lifetime`() = runTest {
|
||||||
|
|
|
||||||
|
|
@ -24,11 +24,12 @@ class FakeEnterpriseService(
|
||||||
private val defaultHomeserverListResult: () -> List<String> = { emptyList() },
|
private val defaultHomeserverListResult: () -> List<String> = { emptyList() },
|
||||||
private val isAllowedToConnectToHomeserverResult: (String) -> Boolean = { lambdaError() },
|
private val isAllowedToConnectToHomeserverResult: (String) -> Boolean = { lambdaError() },
|
||||||
initialSemanticColors: SemanticColorsLightDark = SemanticColorsLightDark.default,
|
initialSemanticColors: SemanticColorsLightDark = SemanticColorsLightDark.default,
|
||||||
|
initialBrandColor: Color? = null,
|
||||||
private val overrideBrandColorResult: (SessionId?, String?) -> Unit = { _, _ -> lambdaError() },
|
private val overrideBrandColorResult: (SessionId?, String?) -> Unit = { _, _ -> lambdaError() },
|
||||||
private val firebasePushGatewayResult: () -> String? = { lambdaError() },
|
private val firebasePushGatewayResult: () -> String? = { lambdaError() },
|
||||||
private val unifiedPushDefaultPushGatewayResult: () -> String? = { lambdaError() },
|
private val unifiedPushDefaultPushGatewayResult: () -> String? = { lambdaError() },
|
||||||
) : EnterpriseService {
|
) : EnterpriseService {
|
||||||
private val brandColorState = MutableStateFlow<Color?>(null)
|
private val brandColorState = MutableStateFlow(initialBrandColor)
|
||||||
private val semanticColorsState = MutableStateFlow(initialSemanticColors)
|
private val semanticColorsState = MutableStateFlow(initialSemanticColors)
|
||||||
|
|
||||||
override suspend fun isEnterpriseUser(sessionId: SessionId): Boolean = simulateLongTask {
|
override suspend fun isEnterpriseUser(sessionId: SessionId): Boolean = simulateLongTask {
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ dependencies {
|
||||||
implementation(projects.libraries.designsystem)
|
implementation(projects.libraries.designsystem)
|
||||||
implementation(projects.libraries.matrix.api)
|
implementation(projects.libraries.matrix.api)
|
||||||
implementation(projects.libraries.roomselect.api)
|
implementation(projects.libraries.roomselect.api)
|
||||||
|
implementation(projects.libraries.uiStrings)
|
||||||
|
|
||||||
testCommonDependencies(libs, true)
|
testCommonDependencies(libs, true)
|
||||||
testImplementation(projects.libraries.matrix.test)
|
testImplementation(projects.libraries.matrix.test)
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ import io.element.android.libraries.matrix.api.timeline.TimelineProvider
|
||||||
import io.element.android.libraries.matrix.api.timeline.getActiveTimeline
|
import io.element.android.libraries.matrix.api.timeline.getActiveTimeline
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
@AssistedInject
|
@AssistedInject
|
||||||
class ForwardMessagesPresenter(
|
class ForwardMessagesPresenter(
|
||||||
|
|
@ -54,7 +55,7 @@ class ForwardMessagesPresenter(
|
||||||
|
|
||||||
return ForwardMessagesState(
|
return ForwardMessagesState(
|
||||||
forwardAction = forwardingActionState.value,
|
forwardAction = forwardingActionState.value,
|
||||||
eventSink = { handleEvents(it) }
|
eventSink = ::handleEvents,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -63,7 +64,11 @@ class ForwardMessagesPresenter(
|
||||||
roomIds: List<RoomId>,
|
roomIds: List<RoomId>,
|
||||||
) = launch {
|
) = launch {
|
||||||
suspend {
|
suspend {
|
||||||
timelineProvider.getActiveTimeline().forwardEvent(eventId, roomIds).getOrThrow()
|
timelineProvider.getActiveTimeline().forwardEvent(eventId, roomIds)
|
||||||
|
.onFailure {
|
||||||
|
Timber.e(it, "Error while forwarding event")
|
||||||
|
}
|
||||||
|
.getOrThrow()
|
||||||
roomIds
|
roomIds
|
||||||
}.runCatchingUpdatingState(forwardingActionState)
|
}.runCatchingUpdatingState(forwardingActionState)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,11 +8,13 @@
|
||||||
package io.element.android.features.forward.impl
|
package io.element.android.features.forward.impl
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import io.element.android.libraries.designsystem.components.async.AsyncActionView
|
import io.element.android.libraries.designsystem.components.async.AsyncActionView
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ForwardMessagesView(
|
fun ForwardMessagesView(
|
||||||
|
|
@ -24,6 +26,9 @@ fun ForwardMessagesView(
|
||||||
onSuccess = {
|
onSuccess = {
|
||||||
onForwardSuccess(it)
|
onForwardSuccess(it)
|
||||||
},
|
},
|
||||||
|
errorMessage = {
|
||||||
|
stringResource(id = CommonStrings.error_unknown)
|
||||||
|
},
|
||||||
onErrorDismiss = {
|
onErrorDismiss = {
|
||||||
state.eventSink(ForwardMessagesEvents.ClearError)
|
state.eventSink(ForwardMessagesEvents.ClearError)
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,9 @@ import androidx.compose.runtime.remember
|
||||||
import dev.zacsweers.metro.Inject
|
import dev.zacsweers.metro.Inject
|
||||||
import io.element.android.features.logout.api.direct.DirectLogoutEvents
|
import io.element.android.features.logout.api.direct.DirectLogoutEvents
|
||||||
import io.element.android.features.logout.api.direct.DirectLogoutState
|
import io.element.android.features.logout.api.direct.DirectLogoutState
|
||||||
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
|
import io.element.android.libraries.core.coroutine.mapState
|
||||||
import io.element.android.libraries.matrix.api.encryption.EncryptionService
|
import io.element.android.libraries.matrix.api.encryption.EncryptionService
|
||||||
import io.element.android.libraries.matrix.api.encryption.RecoveryState
|
import io.element.android.libraries.matrix.api.encryption.RecoveryState
|
||||||
|
|
||||||
|
|
@ -27,8 +29,33 @@ class ChooseSelfVerificationModePresenter(
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): ChooseSelfVerificationModeState {
|
override fun present(): ChooseSelfVerificationModeState {
|
||||||
val hasDevicesToVerifyAgainst by encryptionService.hasDevicesToVerifyAgainst.collectAsState()
|
val hasDevicesToVerifyAgainst by encryptionService.hasDevicesToVerifyAgainst.collectAsState()
|
||||||
val recoveryState by encryptionService.recoveryStateStateFlow.collectAsState()
|
val canEnterRecoveryKey by encryptionService.recoveryStateStateFlow
|
||||||
val canEnterRecoveryKey by remember { derivedStateOf { recoveryState == RecoveryState.INCOMPLETE } }
|
.mapState { recoveryState ->
|
||||||
|
when (recoveryState) {
|
||||||
|
RecoveryState.WAITING_FOR_SYNC,
|
||||||
|
RecoveryState.UNKNOWN -> AsyncData.Loading()
|
||||||
|
RecoveryState.INCOMPLETE -> AsyncData.Success(true)
|
||||||
|
RecoveryState.ENABLED,
|
||||||
|
RecoveryState.DISABLED -> AsyncData.Success(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.collectAsState()
|
||||||
|
val buttonsState by remember {
|
||||||
|
derivedStateOf {
|
||||||
|
val canUseAnotherDevice = hasDevicesToVerifyAgainst.dataOrNull()
|
||||||
|
val canEnterRecoveryKey = canEnterRecoveryKey.dataOrNull()
|
||||||
|
if (canUseAnotherDevice == null || canEnterRecoveryKey == null) {
|
||||||
|
AsyncData.Loading()
|
||||||
|
} else {
|
||||||
|
AsyncData.Success(
|
||||||
|
ChooseSelfVerificationModeState.ButtonsState(
|
||||||
|
canUseAnotherDevice = canUseAnotherDevice,
|
||||||
|
canEnterRecoveryKey = canEnterRecoveryKey,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val directLogoutState = directLogoutPresenter.present()
|
val directLogoutState = directLogoutPresenter.present()
|
||||||
|
|
||||||
|
|
@ -39,8 +66,7 @@ class ChooseSelfVerificationModePresenter(
|
||||||
}
|
}
|
||||||
|
|
||||||
return ChooseSelfVerificationModeState(
|
return ChooseSelfVerificationModeState(
|
||||||
canUseAnotherDevice = hasDevicesToVerifyAgainst,
|
buttonsState = buttonsState,
|
||||||
canEnterRecoveryKey = canEnterRecoveryKey,
|
|
||||||
directLogoutState = directLogoutState,
|
directLogoutState = directLogoutState,
|
||||||
eventSink = ::eventHandler,
|
eventSink = ::eventHandler,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -8,10 +8,15 @@
|
||||||
package io.element.android.features.ftue.impl.sessionverification.choosemode
|
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
|
||||||
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
|
|
||||||
data class ChooseSelfVerificationModeState(
|
data class ChooseSelfVerificationModeState(
|
||||||
val canUseAnotherDevice: Boolean,
|
val buttonsState: AsyncData<ButtonsState>,
|
||||||
val canEnterRecoveryKey: Boolean,
|
|
||||||
val directLogoutState: DirectLogoutState,
|
val directLogoutState: DirectLogoutState,
|
||||||
val eventSink: (ChooseSelfVerificationModeEvent) -> Unit,
|
val eventSink: (ChooseSelfVerificationModeEvent) -> Unit,
|
||||||
)
|
) {
|
||||||
|
data class ButtonsState(
|
||||||
|
val canUseAnotherDevice: Boolean,
|
||||||
|
val canEnterRecoveryKey: Boolean,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,23 +9,49 @@ package io.element.android.features.ftue.impl.sessionverification.choosemode
|
||||||
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
import io.element.android.features.logout.api.direct.aDirectLogoutState
|
import io.element.android.features.logout.api.direct.aDirectLogoutState
|
||||||
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
|
|
||||||
class ChooseSelfVerificationModeStateProvider :
|
class ChooseSelfVerificationModeStateProvider :
|
||||||
PreviewParameterProvider<ChooseSelfVerificationModeState> {
|
PreviewParameterProvider<ChooseSelfVerificationModeState> {
|
||||||
override val values = sequenceOf(
|
override val values = sequenceOf(
|
||||||
aChooseSelfVerificationModeState(canUseAnotherDevice = false, canEnterRecoveryKey = true),
|
aChooseSelfVerificationModeState(
|
||||||
aChooseSelfVerificationModeState(canUseAnotherDevice = false, canEnterRecoveryKey = false),
|
buttonsState = AsyncData.Success(
|
||||||
aChooseSelfVerificationModeState(canUseAnotherDevice = true, canEnterRecoveryKey = true),
|
aButtonsState(canUseAnotherDevice = false, canEnterRecoveryKey = true),
|
||||||
aChooseSelfVerificationModeState(canUseAnotherDevice = true, canEnterRecoveryKey = false),
|
),
|
||||||
|
),
|
||||||
|
aChooseSelfVerificationModeState(
|
||||||
|
buttonsState = AsyncData.Success(
|
||||||
|
aButtonsState(canUseAnotherDevice = false, canEnterRecoveryKey = false),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
aChooseSelfVerificationModeState(
|
||||||
|
buttonsState = AsyncData.Success(
|
||||||
|
aButtonsState(canUseAnotherDevice = true, canEnterRecoveryKey = true),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
aChooseSelfVerificationModeState(
|
||||||
|
buttonsState = AsyncData.Success(
|
||||||
|
aButtonsState(canUseAnotherDevice = true, canEnterRecoveryKey = false),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
aChooseSelfVerificationModeState(
|
||||||
|
buttonsState = AsyncData.Loading(),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun aChooseSelfVerificationModeState(
|
fun aChooseSelfVerificationModeState(
|
||||||
canUseAnotherDevice: Boolean = true,
|
buttonsState: AsyncData<ChooseSelfVerificationModeState.ButtonsState> = AsyncData.Success(aButtonsState()),
|
||||||
canEnterRecoveryKey: Boolean = true,
|
|
||||||
) = ChooseSelfVerificationModeState(
|
) = ChooseSelfVerificationModeState(
|
||||||
canUseAnotherDevice = canUseAnotherDevice,
|
buttonsState = buttonsState,
|
||||||
canEnterRecoveryKey = canEnterRecoveryKey,
|
|
||||||
directLogoutState = aDirectLogoutState(),
|
directLogoutState = aDirectLogoutState(),
|
||||||
eventSink = {},
|
eventSink = {},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
fun aButtonsState(
|
||||||
|
canUseAnotherDevice: Boolean = true,
|
||||||
|
canEnterRecoveryKey: Boolean = true,
|
||||||
|
) = ChooseSelfVerificationModeState.ButtonsState(
|
||||||
|
canUseAnotherDevice = canUseAnotherDevice,
|
||||||
|
canEnterRecoveryKey = canEnterRecoveryKey,
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ import androidx.compose.ui.unit.dp
|
||||||
import io.element.android.compound.theme.ElementTheme
|
import io.element.android.compound.theme.ElementTheme
|
||||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||||
import io.element.android.features.ftue.impl.R
|
import io.element.android.features.ftue.impl.R
|
||||||
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule
|
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.molecules.IconTitleSubtitleMolecule
|
||||||
import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
|
import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
|
||||||
|
|
@ -50,7 +51,6 @@ fun ChooseSelfVerificationModeView(
|
||||||
BackHandler {
|
BackHandler {
|
||||||
activity?.finish()
|
activity?.finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
HeaderFooterPage(
|
HeaderFooterPage(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
topBar = {
|
topBar = {
|
||||||
|
|
@ -73,29 +73,12 @@ fun ChooseSelfVerificationModeView(
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
footer = {
|
footer = {
|
||||||
ButtonColumnMolecule(
|
ChooseSelfVerificationModeButtons(
|
||||||
modifier = Modifier.padding(bottom = 16.dp)
|
state = state,
|
||||||
) {
|
onUseAnotherDevice = onUseAnotherDevice,
|
||||||
if (state.canUseAnotherDevice) {
|
onUseRecoveryKey = onUseRecoveryKey,
|
||||||
Button(
|
onResetKey = onResetKey,
|
||||||
modifier = Modifier.fillMaxWidth(),
|
)
|
||||||
text = stringResource(R.string.screen_identity_use_another_device),
|
|
||||||
onClick = onUseAnotherDevice,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (state.canEnterRecoveryKey) {
|
|
||||||
Button(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
text = stringResource(R.string.screen_session_verification_enter_recovery_key),
|
|
||||||
onClick = onUseRecoveryKey,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
OutlinedButton(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
text = stringResource(R.string.screen_identity_confirmation_cannot_confirm),
|
|
||||||
onClick = onResetKey,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
|
|
@ -113,6 +96,53 @@ fun ChooseSelfVerificationModeView(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ChooseSelfVerificationModeButtons(
|
||||||
|
state: ChooseSelfVerificationModeState,
|
||||||
|
onUseAnotherDevice: () -> Unit,
|
||||||
|
onUseRecoveryKey: () -> Unit,
|
||||||
|
onResetKey: () -> Unit,
|
||||||
|
) {
|
||||||
|
ButtonColumnMolecule(
|
||||||
|
modifier = Modifier.padding(bottom = 16.dp)
|
||||||
|
) {
|
||||||
|
when (state.buttonsState) {
|
||||||
|
AsyncData.Uninitialized,
|
||||||
|
is AsyncData.Failure,
|
||||||
|
is AsyncData.Loading -> {
|
||||||
|
Button(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
enabled = false,
|
||||||
|
showProgress = true,
|
||||||
|
text = stringResource(CommonStrings.common_loading),
|
||||||
|
onClick = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is AsyncData.Success -> {
|
||||||
|
if (state.buttonsState.data.canUseAnotherDevice) {
|
||||||
|
Button(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
text = stringResource(R.string.screen_identity_use_another_device),
|
||||||
|
onClick = onUseAnotherDevice,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (state.buttonsState.data.canEnterRecoveryKey) {
|
||||||
|
Button(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
text = stringResource(R.string.screen_session_verification_enter_recovery_key),
|
||||||
|
onClick = onUseRecoveryKey,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
OutlinedButton(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
text = stringResource(R.string.screen_identity_confirmation_cannot_confirm),
|
||||||
|
onClick = onResetKey,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@PreviewsDayNight
|
@PreviewsDayNight
|
||||||
@Composable
|
@Composable
|
||||||
internal fun ChooseSelfVerificationModeViewPreview(
|
internal fun ChooseSelfVerificationModeViewPreview(
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.features.logout.api.direct.DirectLogoutEvents
|
import io.element.android.features.logout.api.direct.DirectLogoutEvents
|
||||||
import io.element.android.features.logout.api.direct.DirectLogoutState
|
import io.element.android.features.logout.api.direct.DirectLogoutState
|
||||||
import io.element.android.features.logout.api.direct.aDirectLogoutState
|
import io.element.android.features.logout.api.direct.aDirectLogoutState
|
||||||
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.matrix.api.encryption.RecoveryState
|
import io.element.android.libraries.matrix.api.encryption.RecoveryState
|
||||||
import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService
|
import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService
|
||||||
|
|
@ -22,23 +23,92 @@ import org.junit.Test
|
||||||
|
|
||||||
class ChooseSessionVerificationModePresenterTest {
|
class ChooseSessionVerificationModePresenterTest {
|
||||||
@Test
|
@Test
|
||||||
fun `initial state - is relayed from EncryptionService`() = runTest {
|
fun `present - initial state`() = runTest {
|
||||||
val encryptionService = FakeEncryptionService().apply {
|
val presenter = createPresenter()
|
||||||
// Has device to verify against
|
|
||||||
emitHasDevicesToVerifyAgainst(false)
|
|
||||||
// Can enter recovery key
|
|
||||||
emitRecoveryState(RecoveryState.INCOMPLETE)
|
|
||||||
}
|
|
||||||
val presenter = createPresenter(encryptionService = encryptionService)
|
|
||||||
presenter.test {
|
presenter.test {
|
||||||
awaitItem().run {
|
awaitItem().run {
|
||||||
assertThat(canUseAnotherDevice).isFalse()
|
assertThat(buttonsState.isLoading()).isTrue()
|
||||||
assertThat(canEnterRecoveryKey).isTrue()
|
|
||||||
assertThat(directLogoutState.logoutAction.isUninitialized()).isTrue()
|
assertThat(directLogoutState.logoutAction.isUninitialized()).isTrue()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - state is relayed from EncryptionService, order 1`() = runTest {
|
||||||
|
val encryptionService = FakeEncryptionService()
|
||||||
|
val presenter = createPresenter(encryptionService = encryptionService)
|
||||||
|
presenter.test {
|
||||||
|
assertThat(awaitItem().buttonsState.isLoading()).isTrue()
|
||||||
|
// Has device to verify against
|
||||||
|
encryptionService.emitHasDevicesToVerifyAgainst(AsyncData.Success(false))
|
||||||
|
// Can enter recovery key
|
||||||
|
encryptionService.emitRecoveryState(RecoveryState.DISABLED)
|
||||||
|
assertThat(awaitItem().buttonsState.dataOrNull()).isEqualTo(
|
||||||
|
ChooseSelfVerificationModeState.ButtonsState(
|
||||||
|
canUseAnotherDevice = false,
|
||||||
|
canEnterRecoveryKey = false,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - state is relayed from EncryptionService, order 2`() = runTest {
|
||||||
|
val encryptionService = FakeEncryptionService()
|
||||||
|
val presenter = createPresenter(encryptionService = encryptionService)
|
||||||
|
presenter.test {
|
||||||
|
assertThat(awaitItem().buttonsState.isLoading()).isTrue()
|
||||||
|
// Can enter recovery key
|
||||||
|
encryptionService.emitRecoveryState(RecoveryState.DISABLED)
|
||||||
|
// Has device to verify against
|
||||||
|
encryptionService.emitHasDevicesToVerifyAgainst(AsyncData.Success(false))
|
||||||
|
assertThat(awaitItem().buttonsState.dataOrNull()).isEqualTo(
|
||||||
|
ChooseSelfVerificationModeState.ButtonsState(
|
||||||
|
canUseAnotherDevice = false,
|
||||||
|
canEnterRecoveryKey = false,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - can use another device`() = runTest {
|
||||||
|
val encryptionService = FakeEncryptionService()
|
||||||
|
val presenter = createPresenter(encryptionService = encryptionService)
|
||||||
|
presenter.test {
|
||||||
|
assertThat(awaitItem().buttonsState.isLoading()).isTrue()
|
||||||
|
// Can enter recovery key
|
||||||
|
encryptionService.emitRecoveryState(RecoveryState.DISABLED)
|
||||||
|
// Has device to verify against
|
||||||
|
encryptionService.emitHasDevicesToVerifyAgainst(AsyncData.Success(true))
|
||||||
|
assertThat(awaitItem().buttonsState.dataOrNull()).isEqualTo(
|
||||||
|
ChooseSelfVerificationModeState.ButtonsState(
|
||||||
|
canUseAnotherDevice = true,
|
||||||
|
canEnterRecoveryKey = false,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - can enter recovery key`() = runTest {
|
||||||
|
val encryptionService = FakeEncryptionService()
|
||||||
|
val presenter = createPresenter(encryptionService = encryptionService)
|
||||||
|
presenter.test {
|
||||||
|
assertThat(awaitItem().buttonsState.isLoading()).isTrue()
|
||||||
|
// Can enter recovery key
|
||||||
|
encryptionService.emitRecoveryState(RecoveryState.INCOMPLETE)
|
||||||
|
// Has device to verify against
|
||||||
|
encryptionService.emitHasDevicesToVerifyAgainst(AsyncData.Success(false))
|
||||||
|
assertThat(awaitItem().buttonsState.dataOrNull()).isEqualTo(
|
||||||
|
ChooseSelfVerificationModeState.ButtonsState(
|
||||||
|
canUseAnotherDevice = false,
|
||||||
|
canEnterRecoveryKey = true,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `sing out action triggers a direct logout`() = runTest {
|
fun `sing out action triggers a direct logout`() = runTest {
|
||||||
val logoutEventRecorder = lambdaRecorder<DirectLogoutEvents, Unit> {}
|
val logoutEventRecorder = lambdaRecorder<DirectLogoutEvents, Unit> {}
|
||||||
|
|
@ -49,8 +119,8 @@ class ChooseSessionVerificationModePresenterTest {
|
||||||
presenter.test {
|
presenter.test {
|
||||||
val initial = awaitItem()
|
val initial = awaitItem()
|
||||||
initial.eventSink(ChooseSelfVerificationModeEvent.SignOut)
|
initial.eventSink(ChooseSelfVerificationModeEvent.SignOut)
|
||||||
|
logoutEventRecorder.assertions().isCalledOnce()
|
||||||
logoutEventRecorder.assertions().isCalledOnce().with(value(DirectLogoutEvents.Logout(ignoreSdkError = false)))
|
.with(value(DirectLogoutEvents.Logout(ignoreSdkError = false)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import androidx.compose.ui.test.junit4.AndroidComposeTestRule
|
||||||
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import io.element.android.features.ftue.impl.R
|
import io.element.android.features.ftue.impl.R
|
||||||
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
import io.element.android.tests.testutils.EnsureNeverCalled
|
import io.element.android.tests.testutils.EnsureNeverCalled
|
||||||
import io.element.android.tests.testutils.clickOn
|
import io.element.android.tests.testutils.clickOn
|
||||||
|
|
@ -43,7 +44,7 @@ class ChooseSessionVerificationModeViewTest {
|
||||||
fun `clicking on use another device calls the callback`() {
|
fun `clicking on use another device calls the callback`() {
|
||||||
ensureCalledOnce { callback ->
|
ensureCalledOnce { callback ->
|
||||||
rule.setChooseSelfVerificationModeView(
|
rule.setChooseSelfVerificationModeView(
|
||||||
aChooseSelfVerificationModeState(canUseAnotherDevice = true),
|
aChooseSelfVerificationModeState(AsyncData.Success(aButtonsState(canUseAnotherDevice = true))),
|
||||||
onUseAnotherDevice = callback,
|
onUseAnotherDevice = callback,
|
||||||
)
|
)
|
||||||
rule.clickOn(R.string.screen_identity_use_another_device)
|
rule.clickOn(R.string.screen_identity_use_another_device)
|
||||||
|
|
@ -55,7 +56,7 @@ class ChooseSessionVerificationModeViewTest {
|
||||||
fun `clicking on enter recovery key calls the callback`() {
|
fun `clicking on enter recovery key calls the callback`() {
|
||||||
ensureCalledOnce { callback ->
|
ensureCalledOnce { callback ->
|
||||||
rule.setChooseSelfVerificationModeView(
|
rule.setChooseSelfVerificationModeView(
|
||||||
aChooseSelfVerificationModeState(canEnterRecoveryKey = true),
|
aChooseSelfVerificationModeState(AsyncData.Success(aButtonsState(canEnterRecoveryKey = true))),
|
||||||
onEnterRecoveryKey = callback,
|
onEnterRecoveryKey = callback,
|
||||||
)
|
)
|
||||||
rule.clickOn(R.string.screen_session_verification_enter_recovery_key)
|
rule.clickOn(R.string.screen_session_verification_enter_recovery_key)
|
||||||
|
|
|
||||||
|
|
@ -13,5 +13,4 @@ data class AccountProvider(
|
||||||
val subtitle: String? = null,
|
val subtitle: String? = null,
|
||||||
val isPublic: Boolean = false,
|
val isPublic: Boolean = false,
|
||||||
val isMatrixOrg: Boolean = false,
|
val isMatrixOrg: Boolean = false,
|
||||||
val isValid: Boolean = false,
|
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ open class AccountProviderProvider : PreviewParameterProvider<AccountProvider> {
|
||||||
get() = sequenceOf(
|
get() = sequenceOf(
|
||||||
anAccountProvider(),
|
anAccountProvider(),
|
||||||
anAccountProvider().copy(subtitle = null),
|
anAccountProvider().copy(subtitle = null),
|
||||||
anAccountProvider().copy(subtitle = null, title = "invalid", isValid = false),
|
anAccountProvider().copy(subtitle = null, title = "invalid"),
|
||||||
anAccountProvider().copy(subtitle = null, title = "Other", isPublic = false, isMatrixOrg = false),
|
anAccountProvider().copy(subtitle = null, title = "Other", isPublic = false, isMatrixOrg = false),
|
||||||
// Add other state here
|
// Add other state here
|
||||||
)
|
)
|
||||||
|
|
@ -26,11 +26,9 @@ fun anAccountProvider(
|
||||||
subtitle: String? = "Matrix.org is an open network for secure, decentralized communication.",
|
subtitle: String? = "Matrix.org is an open network for secure, decentralized communication.",
|
||||||
isPublic: Boolean = true,
|
isPublic: Boolean = true,
|
||||||
isMatrixOrg: Boolean = true,
|
isMatrixOrg: Boolean = true,
|
||||||
isValid: Boolean = true,
|
|
||||||
) = AccountProvider(
|
) = AccountProvider(
|
||||||
url = url,
|
url = url,
|
||||||
subtitle = subtitle,
|
subtitle = subtitle,
|
||||||
isPublic = isPublic,
|
isPublic = isPublic,
|
||||||
isMatrixOrg = isMatrixOrg,
|
isMatrixOrg = isMatrixOrg,
|
||||||
isValid = isValid,
|
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,4 @@ package io.element.android.features.login.impl.resolver
|
||||||
data class HomeserverData(
|
data class HomeserverData(
|
||||||
// The computed homeserver url, for which a wellknown file has been retrieved, or just a valid Url
|
// The computed homeserver url, for which a wellknown file has been retrieved, or just a valid Url
|
||||||
val homeserverUrl: String,
|
val homeserverUrl: String,
|
||||||
// True if a wellknown file has been found and is valid. If false, it means that the [homeserverUrl] is valid
|
|
||||||
val isWellknownValid: Boolean,
|
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -8,19 +8,16 @@
|
||||||
package io.element.android.features.login.impl.resolver
|
package io.element.android.features.login.impl.resolver
|
||||||
|
|
||||||
import dev.zacsweers.metro.Inject
|
import dev.zacsweers.metro.Inject
|
||||||
import io.element.android.libraries.core.bool.orFalse
|
|
||||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||||
import io.element.android.libraries.core.coroutine.parallelMap
|
import io.element.android.libraries.core.coroutine.parallelMap
|
||||||
import io.element.android.libraries.core.data.tryOrNull
|
|
||||||
import io.element.android.libraries.core.uri.ensureProtocol
|
import io.element.android.libraries.core.uri.ensureProtocol
|
||||||
import io.element.android.libraries.core.uri.isValidUrl
|
import io.element.android.libraries.core.uri.isValidUrl
|
||||||
import io.element.android.libraries.wellknown.api.WellKnown
|
import io.element.android.libraries.matrix.api.auth.HomeServerLoginCompatibilityChecker
|
||||||
import io.element.android.libraries.wellknown.api.WellknownRetriever
|
|
||||||
import kotlinx.coroutines.currentCoroutineContext
|
import kotlinx.coroutines.currentCoroutineContext
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.flow
|
import kotlinx.coroutines.flow.flow
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.coroutines.withTimeout
|
import timber.log.Timber
|
||||||
import java.util.Collections
|
import java.util.Collections
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -29,7 +26,7 @@ import java.util.Collections
|
||||||
@Inject
|
@Inject
|
||||||
class HomeserverResolver(
|
class HomeserverResolver(
|
||||||
private val dispatchers: CoroutineDispatchers,
|
private val dispatchers: CoroutineDispatchers,
|
||||||
private val wellknownRetriever: WellknownRetriever,
|
private val homeServerLoginCompatibilityChecker: HomeServerLoginCompatibilityChecker,
|
||||||
) {
|
) {
|
||||||
fun resolve(userInput: String): Flow<List<HomeserverData>> = flow {
|
fun resolve(userInput: String): Flow<List<HomeserverData>> = flow {
|
||||||
val flowContext = currentCoroutineContext()
|
val flowContext = currentCoroutineContext()
|
||||||
|
|
@ -41,20 +38,14 @@ class HomeserverResolver(
|
||||||
// Run all the requests in parallel
|
// Run all the requests in parallel
|
||||||
withContext(dispatchers.io) {
|
withContext(dispatchers.io) {
|
||||||
list.parallelMap { url ->
|
list.parallelMap { url ->
|
||||||
val wellKnown = tryOrNull {
|
val isValid = homeServerLoginCompatibilityChecker.check(url)
|
||||||
withTimeout(5000) {
|
.onFailure { Timber.w(it, "Failed to check compatibility with homeserver $url") }
|
||||||
wellknownRetriever.getWellKnown(url)
|
.getOrNull()
|
||||||
}
|
?: return@parallelMap
|
||||||
}
|
|
||||||
val isValid = wellKnown?.dataOrNull()?.isValid().orFalse()
|
// Emit the list as soon as possible
|
||||||
if (isValid) {
|
if (isValid) {
|
||||||
// Emit the list as soon as possible
|
currentList.add(HomeserverData(homeserverUrl = url))
|
||||||
currentList.add(
|
|
||||||
HomeserverData(
|
|
||||||
homeserverUrl = url,
|
|
||||||
isWellknownValid = true,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
withContext(flowContext) {
|
withContext(flowContext) {
|
||||||
emit(currentList.toList())
|
emit(currentList.toList())
|
||||||
}
|
}
|
||||||
|
|
@ -63,14 +54,7 @@ class HomeserverResolver(
|
||||||
}
|
}
|
||||||
// If list is empty, and the user has entered an URL, do not block the user.
|
// If list is empty, and the user has entered an URL, do not block the user.
|
||||||
if (currentList.isEmpty() && trimmedUserInput.isValidUrl()) {
|
if (currentList.isEmpty() && trimmedUserInput.isValidUrl()) {
|
||||||
emit(
|
emit(listOf(HomeserverData(homeserverUrl = trimmedUserInput)))
|
||||||
listOf(
|
|
||||||
HomeserverData(
|
|
||||||
homeserverUrl = trimmedUserInput,
|
|
||||||
isWellknownValid = false,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -88,7 +72,3 @@ class HomeserverResolver(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun WellKnown.isValid(): Boolean {
|
|
||||||
return homeServer?.baseURL?.isNotBlank().orFalse()
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,6 @@ class ChangeAccountProviderPresenter(
|
||||||
subtitle = null,
|
subtitle = null,
|
||||||
isPublic = url == AuthenticationConfig.MATRIX_ORG_URL,
|
isPublic = url == AuthenticationConfig.MATRIX_ORG_URL,
|
||||||
isMatrixOrg = url == AuthenticationConfig.MATRIX_ORG_URL,
|
isMatrixOrg = url == AuthenticationConfig.MATRIX_ORG_URL,
|
||||||
isValid = true,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.toImmutableList()
|
.toImmutableList()
|
||||||
|
|
|
||||||
|
|
@ -67,7 +67,6 @@ class ChooseAccountProviderPresenter(
|
||||||
subtitle = null,
|
subtitle = null,
|
||||||
isPublic = url == AuthenticationConfig.MATRIX_ORG_URL,
|
isPublic = url == AuthenticationConfig.MATRIX_ORG_URL,
|
||||||
isMatrixOrg = url == AuthenticationConfig.MATRIX_ORG_URL,
|
isMatrixOrg = url == AuthenticationConfig.MATRIX_ORG_URL,
|
||||||
isValid = true,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.toImmutableList()
|
.toImmutableList()
|
||||||
|
|
|
||||||
|
|
@ -90,7 +90,7 @@ class OnBoardingPresenter(
|
||||||
}
|
}
|
||||||
val isAddingAccount by produceState(initialValue = false) {
|
val isAddingAccount by produceState(initialValue = false) {
|
||||||
// We are adding an account if there is at least one session already stored
|
// We are adding an account if there is at least one session already stored
|
||||||
value = sessionStore.getAllSessions().isNotEmpty()
|
value = sessionStore.numberOfSessions() > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
val loginMode by loginHelper.collectLoginMode()
|
val loginMode by loginHelper.collectLoginMode()
|
||||||
|
|
|
||||||
|
|
@ -57,14 +57,14 @@ class SearchAccountProviderPresenter(
|
||||||
userInput = userInput,
|
userInput = userInput,
|
||||||
userInputResult = data.value,
|
userInputResult = data.value,
|
||||||
changeServerState = changeServerState,
|
changeServerState = changeServerState,
|
||||||
eventSink = ::handleEvents
|
eventSink = ::handleEvents,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun CoroutineScope.onUserInput(userInput: String, data: MutableState<AsyncData<List<HomeserverData>>>) = launch {
|
private fun CoroutineScope.onUserInput(userInput: String, data: MutableState<AsyncData<List<HomeserverData>>>) = launch {
|
||||||
data.value = AsyncData.Uninitialized
|
data.value = AsyncData.Uninitialized
|
||||||
// Debounce
|
// Debounce
|
||||||
delay(300)
|
delay(500)
|
||||||
data.value = AsyncData.Loading()
|
data.value = AsyncData.Loading()
|
||||||
homeserverResolver.resolve(userInput).collect {
|
homeserverResolver.resolve(userInput).collect {
|
||||||
data.value = AsyncData.Success(it)
|
data.value = AsyncData.Success(it)
|
||||||
|
|
|
||||||
|
|
@ -34,18 +34,14 @@ fun aSearchAccountProviderState(
|
||||||
|
|
||||||
fun aHomeserverDataList(): List<HomeserverData> {
|
fun aHomeserverDataList(): List<HomeserverData> {
|
||||||
return listOf(
|
return listOf(
|
||||||
aHomeserverData(isWellknownValid = true),
|
aHomeserverData(homeserverUrl = AuthenticationConfig.MATRIX_ORG_URL),
|
||||||
aHomeserverData(homeserverUrl = "https://no.sliding.sync", isWellknownValid = true),
|
aHomeserverData(homeserverUrl = "https://no.sliding.sync"),
|
||||||
aHomeserverData(homeserverUrl = "https://invalid", isWellknownValid = false),
|
aHomeserverData(homeserverUrl = "https://invalid"),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun aHomeserverData(
|
fun aHomeserverData(
|
||||||
homeserverUrl: String = AuthenticationConfig.MATRIX_ORG_URL,
|
homeserverUrl: String = AuthenticationConfig.MATRIX_ORG_URL,
|
||||||
isWellknownValid: Boolean = true,
|
|
||||||
): HomeserverData {
|
): HomeserverData {
|
||||||
return HomeserverData(
|
return HomeserverData(homeserverUrl = homeserverUrl,)
|
||||||
homeserverUrl = homeserverUrl,
|
|
||||||
isWellknownValid = isWellknownValid,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -192,7 +192,6 @@ private fun HomeserverData.toAccountProvider(): AccountProvider {
|
||||||
// There is no need to know for other servers right now
|
// There is no need to know for other servers right now
|
||||||
isPublic = isMatrixOrg,
|
isPublic = isMatrixOrg,
|
||||||
isMatrixOrg = isMatrixOrg,
|
isMatrixOrg = isMatrixOrg,
|
||||||
isValid = isWellknownValid,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,6 @@ class AccountProviderDataSourceTest {
|
||||||
subtitle = null,
|
subtitle = null,
|
||||||
isPublic = true,
|
isPublic = true,
|
||||||
isMatrixOrg = true,
|
isMatrixOrg = true,
|
||||||
isValid = false,
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -55,7 +54,6 @@ class AccountProviderDataSourceTest {
|
||||||
subtitle = null,
|
subtitle = null,
|
||||||
isPublic = true,
|
isPublic = true,
|
||||||
isMatrixOrg = true,
|
isMatrixOrg = true,
|
||||||
isValid = false,
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -77,7 +75,6 @@ class AccountProviderDataSourceTest {
|
||||||
subtitle = null,
|
subtitle = null,
|
||||||
isPublic = true,
|
isPublic = true,
|
||||||
isMatrixOrg = true,
|
isMatrixOrg = true,
|
||||||
isValid = false,
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -98,7 +95,6 @@ class AccountProviderDataSourceTest {
|
||||||
subtitle = null,
|
subtitle = null,
|
||||||
isPublic = false,
|
isPublic = false,
|
||||||
isMatrixOrg = false,
|
isMatrixOrg = false,
|
||||||
isValid = false,
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
sut.reset()
|
sut.reset()
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,6 @@ class ChangeAccountProviderPresenterTest {
|
||||||
subtitle = null,
|
subtitle = null,
|
||||||
isPublic = true,
|
isPublic = true,
|
||||||
isMatrixOrg = true,
|
isMatrixOrg = true,
|
||||||
isValid = true,
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
@ -76,7 +75,6 @@ class ChangeAccountProviderPresenterTest {
|
||||||
subtitle = null,
|
subtitle = null,
|
||||||
isPublic = true,
|
isPublic = true,
|
||||||
isMatrixOrg = true,
|
isMatrixOrg = true,
|
||||||
isValid = true,
|
|
||||||
),
|
),
|
||||||
AccountProvider(
|
AccountProvider(
|
||||||
url = "https://element.io",
|
url = "https://element.io",
|
||||||
|
|
@ -84,7 +82,6 @@ class ChangeAccountProviderPresenterTest {
|
||||||
subtitle = null,
|
subtitle = null,
|
||||||
isPublic = false,
|
isPublic = false,
|
||||||
isMatrixOrg = false,
|
isMatrixOrg = false,
|
||||||
isValid = true,
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
@ -114,7 +111,6 @@ class ChangeAccountProviderPresenterTest {
|
||||||
subtitle = null,
|
subtitle = null,
|
||||||
isPublic = true,
|
isPublic = true,
|
||||||
isMatrixOrg = true,
|
isMatrixOrg = true,
|
||||||
isValid = true,
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -37,14 +37,12 @@ class ChooseAccountProviderPresenterTest {
|
||||||
subtitle = null,
|
subtitle = null,
|
||||||
isPublic = false,
|
isPublic = false,
|
||||||
isMatrixOrg = false,
|
isMatrixOrg = false,
|
||||||
isValid = true,
|
|
||||||
)
|
)
|
||||||
val accountProvider2 = AccountProvider(
|
val accountProvider2 = AccountProvider(
|
||||||
url = ACCOUNT_PROVIDER_FROM_CONFIG_2.ensureProtocol(),
|
url = ACCOUNT_PROVIDER_FROM_CONFIG_2.ensureProtocol(),
|
||||||
subtitle = null,
|
subtitle = null,
|
||||||
isPublic = false,
|
isPublic = false,
|
||||||
isMatrixOrg = false,
|
isMatrixOrg = false,
|
||||||
isValid = true,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,12 +13,8 @@ import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.features.login.impl.changeserver.aChangeServerState
|
import io.element.android.features.login.impl.changeserver.aChangeServerState
|
||||||
import io.element.android.features.login.impl.resolver.HomeserverResolver
|
import io.element.android.features.login.impl.resolver.HomeserverResolver
|
||||||
import io.element.android.features.wellknown.test.FakeWellknownRetriever
|
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
import io.element.android.libraries.matrix.test.A_HOMESERVER_URL
|
import io.element.android.libraries.matrix.test.auth.FakeHomeServerLoginCompatibilityChecker
|
||||||
import io.element.android.libraries.wellknown.api.WellKnown
|
|
||||||
import io.element.android.libraries.wellknown.api.WellKnownBaseConfig
|
|
||||||
import io.element.android.libraries.wellknown.api.WellknownRetrieverResult
|
|
||||||
import io.element.android.tests.testutils.WarmUpRule
|
import io.element.android.tests.testutils.WarmUpRule
|
||||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||||
import io.element.android.tests.testutils.lambda.value
|
import io.element.android.tests.testutils.lambda.value
|
||||||
|
|
@ -33,9 +29,9 @@ class SearchAccountProviderPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - initial state`() = runTest {
|
fun `present - initial state`() = runTest {
|
||||||
val fakeWellknownRetriever = FakeWellknownRetriever()
|
val fakeLoginCompatibilityChecker = FakeHomeServerLoginCompatibilityChecker(checkResult = { Result.success(true) })
|
||||||
val presenter = SearchAccountProviderPresenter(
|
val presenter = SearchAccountProviderPresenter(
|
||||||
homeserverResolver = HomeserverResolver(testCoroutineDispatchers(), fakeWellknownRetriever),
|
homeserverResolver = HomeserverResolver(testCoroutineDispatchers(), fakeLoginCompatibilityChecker),
|
||||||
changeServerPresenter = { aChangeServerState() }
|
changeServerPresenter = { aChangeServerState() }
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
|
@ -47,9 +43,35 @@ class SearchAccountProviderPresenterTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - error while checking login compatibility`() = runTest {
|
||||||
|
val fakeLoginCompatibilityChecker = FakeHomeServerLoginCompatibilityChecker(checkResult = { Result.failure(IllegalStateException("Oops")) })
|
||||||
|
val presenter = SearchAccountProviderPresenter(
|
||||||
|
homeserverResolver = HomeserverResolver(testCoroutineDispatchers(), fakeLoginCompatibilityChecker),
|
||||||
|
changeServerPresenter = { aChangeServerState() }
|
||||||
|
)
|
||||||
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
initialState.eventSink.invoke(SearchAccountProviderEvents.UserInput("https://test.org"))
|
||||||
|
val withInputState = awaitItem()
|
||||||
|
assertThat(withInputState.userInput).isEqualTo("https://test.org")
|
||||||
|
assertThat(initialState.userInputResult).isEqualTo(AsyncData.Uninitialized)
|
||||||
|
assertThat(awaitItem().userInputResult).isInstanceOf(AsyncData.Loading::class.java)
|
||||||
|
assertThat(awaitItem().userInputResult).isEqualTo(
|
||||||
|
AsyncData.Success(
|
||||||
|
listOf(
|
||||||
|
aHomeserverData(homeserverUrl = "https://test.org")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - enter text no result`() = runTest {
|
fun `present - enter text no result`() = runTest {
|
||||||
val fakeWellknownRetriever = FakeWellknownRetriever()
|
val fakeWellknownRetriever = FakeHomeServerLoginCompatibilityChecker(checkResult = { Result.success(false) })
|
||||||
val presenter = SearchAccountProviderPresenter(
|
val presenter = SearchAccountProviderPresenter(
|
||||||
homeserverResolver = HomeserverResolver(testCoroutineDispatchers(), fakeWellknownRetriever),
|
homeserverResolver = HomeserverResolver(testCoroutineDispatchers(), fakeWellknownRetriever),
|
||||||
changeServerPresenter = { aChangeServerState() }
|
changeServerPresenter = { aChangeServerState() }
|
||||||
|
|
@ -67,48 +89,20 @@ class SearchAccountProviderPresenterTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `present - enter valid url no wellknown`() = runTest {
|
|
||||||
val fakeWellknownRetriever = FakeWellknownRetriever()
|
|
||||||
val presenter = SearchAccountProviderPresenter(
|
|
||||||
homeserverResolver = HomeserverResolver(testCoroutineDispatchers(), fakeWellknownRetriever),
|
|
||||||
changeServerPresenter = { aChangeServerState() }
|
|
||||||
)
|
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
val initialState = awaitItem()
|
|
||||||
initialState.eventSink.invoke(SearchAccountProviderEvents.UserInput("https://test.org"))
|
|
||||||
val withInputState = awaitItem()
|
|
||||||
assertThat(withInputState.userInput).isEqualTo("https://test.org")
|
|
||||||
assertThat(initialState.userInputResult).isEqualTo(AsyncData.Uninitialized)
|
|
||||||
assertThat(awaitItem().userInputResult).isInstanceOf(AsyncData.Loading::class.java)
|
|
||||||
assertThat(awaitItem().userInputResult).isEqualTo(
|
|
||||||
AsyncData.Success(
|
|
||||||
listOf(
|
|
||||||
aHomeserverData(homeserverUrl = "https://test.org", isWellknownValid = false)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - enter text one result with wellknown`() = runTest {
|
fun `present - enter text one result with wellknown`() = runTest {
|
||||||
val getWellKnownResult = lambdaRecorder<String, WellknownRetrieverResult<WellKnown>> {
|
val checkResult = lambdaRecorder<String, Result<Boolean>> {
|
||||||
when (it) {
|
when (it) {
|
||||||
"https://test.org" -> WellknownRetrieverResult.NotFound
|
"https://test.org" -> Result.success(false)
|
||||||
"https://test.com" -> WellknownRetrieverResult.NotFound
|
"https://test.com" -> Result.success(false)
|
||||||
"https://test.io" -> WellknownRetrieverResult.Success(aWellKnown())
|
"https://test.io" -> Result.success(true)
|
||||||
"https://test" -> WellknownRetrieverResult.NotFound
|
"https://test" -> Result.success(false)
|
||||||
else -> error("should not happen")
|
else -> error("should not happen")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val fakeWellknownRetriever = FakeWellknownRetriever(
|
val fakeLoginCompatibilityChecker = FakeHomeServerLoginCompatibilityChecker(checkResult = checkResult)
|
||||||
getWellKnownResult = getWellKnownResult,
|
|
||||||
)
|
|
||||||
val presenter = SearchAccountProviderPresenter(
|
val presenter = SearchAccountProviderPresenter(
|
||||||
homeserverResolver = HomeserverResolver(testCoroutineDispatchers(), fakeWellknownRetriever),
|
homeserverResolver = HomeserverResolver(testCoroutineDispatchers(), fakeLoginCompatibilityChecker),
|
||||||
changeServerPresenter = { aChangeServerState() }
|
changeServerPresenter = { aChangeServerState() }
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
|
@ -127,7 +121,7 @@ class SearchAccountProviderPresenterTest {
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
getWellKnownResult.assertions().isCalledExactly(4)
|
checkResult.assertions().isCalledExactly(4)
|
||||||
.withSequence(
|
.withSequence(
|
||||||
listOf(value("https://test.org")),
|
listOf(value("https://test.org")),
|
||||||
listOf(value("https://test.com")),
|
listOf(value("https://test.com")),
|
||||||
|
|
@ -139,20 +133,18 @@ class SearchAccountProviderPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - enter text two results with wellknown`() = runTest {
|
fun `present - enter text two results with wellknown`() = runTest {
|
||||||
val getWellKnownResult = lambdaRecorder<String, WellknownRetrieverResult<WellKnown>> {
|
val checkResult = lambdaRecorder<String, Result<Boolean>> {
|
||||||
when (it) {
|
when (it) {
|
||||||
"https://test.org" -> WellknownRetrieverResult.Success(aWellKnown())
|
"https://test.org" -> Result.success(true)
|
||||||
"https://test.com" -> WellknownRetrieverResult.NotFound
|
"https://test.com" -> Result.success(false)
|
||||||
"https://test.io" -> WellknownRetrieverResult.Success(aWellKnown())
|
"https://test.io" -> Result.success(true)
|
||||||
"https://test" -> WellknownRetrieverResult.NotFound
|
"https://test" -> Result.success(false)
|
||||||
else -> error("should not happen")
|
else -> error("should not happen")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val fakeWellknownRetriever = FakeWellknownRetriever(
|
val fakeLoginCompatibilityChecker = FakeHomeServerLoginCompatibilityChecker(checkResult = checkResult)
|
||||||
getWellKnownResult = getWellKnownResult,
|
|
||||||
)
|
|
||||||
val presenter = SearchAccountProviderPresenter(
|
val presenter = SearchAccountProviderPresenter(
|
||||||
homeserverResolver = HomeserverResolver(testCoroutineDispatchers(), fakeWellknownRetriever),
|
homeserverResolver = HomeserverResolver(testCoroutineDispatchers(), fakeLoginCompatibilityChecker),
|
||||||
changeServerPresenter = { aChangeServerState() }
|
changeServerPresenter = { aChangeServerState() }
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
|
@ -179,7 +171,7 @@ class SearchAccountProviderPresenterTest {
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
getWellKnownResult.assertions().isCalledExactly(4)
|
checkResult.assertions().isCalledExactly(4)
|
||||||
.withSequence(
|
.withSequence(
|
||||||
listOf(value("https://test.org")),
|
listOf(value("https://test.org")),
|
||||||
listOf(value("https://test.com")),
|
listOf(value("https://test.com")),
|
||||||
|
|
@ -188,15 +180,4 @@ class SearchAccountProviderPresenterTest {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun aWellKnown(): WellKnown {
|
|
||||||
return WellKnown(
|
|
||||||
homeServer = WellKnownBaseConfig(
|
|
||||||
baseURL = A_HOMESERVER_URL
|
|
||||||
),
|
|
||||||
identityServer = WellKnownBaseConfig(
|
|
||||||
baseURL = A_HOMESERVER_URL
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ interface MessagesEntryPoint : FeatureEntryPoint {
|
||||||
fun navigateToRoomDetails()
|
fun navigateToRoomDetails()
|
||||||
fun navigateToRoomMemberDetails(userId: UserId)
|
fun navigateToRoomMemberDetails(userId: UserId)
|
||||||
fun handlePermalinkClick(data: PermalinkData, pushToBackstack: Boolean)
|
fun handlePermalinkClick(data: PermalinkData, pushToBackstack: Boolean)
|
||||||
fun forwardEvent(eventId: EventId)
|
fun forwardEvent(eventId: EventId, fromPinnedEvents: Boolean)
|
||||||
fun navigateToRoom(roomId: RoomId)
|
fun navigateToRoom(roomId: RoomId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -47,8 +47,8 @@ interface MessagesEntryPoint : FeatureEntryPoint {
|
||||||
params: Params,
|
params: Params,
|
||||||
callback: Callback,
|
callback: Callback,
|
||||||
): Node
|
): Node
|
||||||
}
|
|
||||||
|
|
||||||
interface MessagesEntryPointNode {
|
interface NodeProxy {
|
||||||
suspend fun attachThread(threadId: ThreadId, focusedEventId: EventId?)
|
suspend fun attachThread(threadId: ThreadId, focusedEventId: EventId?)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.messages.api.pinned
|
||||||
|
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.TimelineProvider
|
||||||
|
|
||||||
|
interface PinnedEventsTimelineProvider : TimelineProvider
|
||||||
|
|
@ -32,10 +32,9 @@ import io.element.android.features.location.api.LocationService
|
||||||
import io.element.android.features.location.api.SendLocationEntryPoint
|
import io.element.android.features.location.api.SendLocationEntryPoint
|
||||||
import io.element.android.features.location.api.ShowLocationEntryPoint
|
import io.element.android.features.location.api.ShowLocationEntryPoint
|
||||||
import io.element.android.features.messages.api.MessagesEntryPoint
|
import io.element.android.features.messages.api.MessagesEntryPoint
|
||||||
import io.element.android.features.messages.api.MessagesEntryPointNode
|
|
||||||
import io.element.android.features.messages.impl.attachments.Attachment
|
import io.element.android.features.messages.impl.attachments.Attachment
|
||||||
import io.element.android.features.messages.impl.attachments.preview.AttachmentsPreviewNode
|
import io.element.android.features.messages.impl.attachments.preview.AttachmentsPreviewNode
|
||||||
import io.element.android.features.messages.impl.pinned.PinnedEventsTimelineProvider
|
import io.element.android.features.messages.impl.pinned.DefaultPinnedEventsTimelineProvider
|
||||||
import io.element.android.features.messages.impl.pinned.list.PinnedMessagesListNode
|
import io.element.android.features.messages.impl.pinned.list.PinnedMessagesListNode
|
||||||
import io.element.android.features.messages.impl.report.ReportMessageNode
|
import io.element.android.features.messages.impl.report.ReportMessageNode
|
||||||
import io.element.android.features.messages.impl.threads.ThreadedMessagesNode
|
import io.element.android.features.messages.impl.threads.ThreadedMessagesNode
|
||||||
|
|
@ -115,7 +114,7 @@ class MessagesFlowNode(
|
||||||
private val roomNamesCache: RoomNamesCache,
|
private val roomNamesCache: RoomNamesCache,
|
||||||
private val mentionSpanUpdater: MentionSpanUpdater,
|
private val mentionSpanUpdater: MentionSpanUpdater,
|
||||||
private val mentionSpanTheme: MentionSpanTheme,
|
private val mentionSpanTheme: MentionSpanTheme,
|
||||||
private val pinnedEventsTimelineProvider: PinnedEventsTimelineProvider,
|
private val pinnedEventsTimelineProvider: DefaultPinnedEventsTimelineProvider,
|
||||||
private val timelineController: TimelineController,
|
private val timelineController: TimelineController,
|
||||||
private val knockRequestsListEntryPoint: KnockRequestsListEntryPoint,
|
private val knockRequestsListEntryPoint: KnockRequestsListEntryPoint,
|
||||||
private val dateFormatter: DateFormatter,
|
private val dateFormatter: DateFormatter,
|
||||||
|
|
@ -130,8 +129,7 @@ class MessagesFlowNode(
|
||||||
),
|
),
|
||||||
buildContext = buildContext,
|
buildContext = buildContext,
|
||||||
plugins = plugins,
|
plugins = plugins,
|
||||||
),
|
), MessagesEntryPoint.NodeProxy {
|
||||||
MessagesEntryPointNode {
|
|
||||||
sealed interface NavTarget : Parcelable {
|
sealed interface NavTarget : Parcelable {
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class Messages(val focusedEventId: EventId?) : NavTarget
|
data class Messages(val focusedEventId: EventId?) : NavTarget
|
||||||
|
|
@ -315,9 +313,9 @@ class MessagesFlowNode(
|
||||||
this@MessagesFlowNode.viewInTimeline(eventId)
|
this@MessagesFlowNode.viewInTimeline(eventId)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun forwardEvent(eventId: EventId) {
|
override fun forwardEvent(eventId: EventId, fromPinnedEvents: Boolean) {
|
||||||
// Need to go to the parent because of the overlay
|
// Need to go to the parent because of the overlay
|
||||||
callback.forwardEvent(eventId)
|
callback.forwardEvent(eventId, fromPinnedEvents)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
mediaViewerEntryPoint.createNode(
|
mediaViewerEntryPoint.createNode(
|
||||||
|
|
|
||||||
|
|
@ -122,7 +122,7 @@ class DefaultActionListPresenter(
|
||||||
|
|
||||||
return ActionListState(
|
return ActionListState(
|
||||||
target = target.value,
|
target = target.value,
|
||||||
eventSink = { handleEvents(it) }
|
eventSink = ::handleEvents,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -172,7 +172,7 @@ class DefaultMediaOptimizationSelectorPresenter(
|
||||||
selectedVideoPreset = selectedVideoOptimizationPreset.dataOrNull(),
|
selectedVideoPreset = selectedVideoOptimizationPreset.dataOrNull(),
|
||||||
displayMediaSelectorViews = displayMediaSelectorViews,
|
displayMediaSelectorViews = displayMediaSelectorViews,
|
||||||
displayVideoPresetSelectorDialog = displayVideoPresetSelectorDialog,
|
displayVideoPresetSelectorDialog = displayVideoPresetSelectorDialog,
|
||||||
eventSink = { handleEvent(it) },
|
eventSink = ::handleEvent,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -382,7 +382,7 @@ class MessageComposerPresenter(
|
||||||
suggestions = suggestions.toImmutableList(),
|
suggestions = suggestions.toImmutableList(),
|
||||||
resolveMentionDisplay = resolveMentionDisplay,
|
resolveMentionDisplay = resolveMentionDisplay,
|
||||||
resolveAtRoomMentionDisplay = resolveAtRoomMentionDisplay,
|
resolveAtRoomMentionDisplay = resolveAtRoomMentionDisplay,
|
||||||
eventSink = { handleEvents(it) },
|
eventSink = ::handleEvents,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,9 @@
|
||||||
|
|
||||||
package io.element.android.features.messages.impl.pinned
|
package io.element.android.features.messages.impl.pinned
|
||||||
|
|
||||||
import dev.zacsweers.metro.Inject
|
import dev.zacsweers.metro.ContributesBinding
|
||||||
import dev.zacsweers.metro.SingleIn
|
import dev.zacsweers.metro.SingleIn
|
||||||
|
import io.element.android.features.messages.api.pinned.PinnedEventsTimelineProvider
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||||
import io.element.android.libraries.core.coroutine.mapState
|
import io.element.android.libraries.core.coroutine.mapState
|
||||||
|
|
@ -17,7 +18,6 @@ import io.element.android.libraries.matrix.api.room.CreateTimelineParams
|
||||||
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
||||||
import io.element.android.libraries.matrix.api.sync.SyncService
|
import io.element.android.libraries.matrix.api.sync.SyncService
|
||||||
import io.element.android.libraries.matrix.api.timeline.Timeline
|
import io.element.android.libraries.matrix.api.timeline.Timeline
|
||||||
import io.element.android.libraries.matrix.api.timeline.TimelineProvider
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.coroutineScope
|
import kotlinx.coroutines.coroutineScope
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
|
@ -29,12 +29,12 @@ import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
@SingleIn(RoomScope::class)
|
@SingleIn(RoomScope::class)
|
||||||
@Inject
|
@ContributesBinding(RoomScope::class)
|
||||||
class PinnedEventsTimelineProvider(
|
class DefaultPinnedEventsTimelineProvider(
|
||||||
private val room: JoinedRoom,
|
private val room: JoinedRoom,
|
||||||
private val syncService: SyncService,
|
private val syncService: SyncService,
|
||||||
private val dispatchers: CoroutineDispatchers,
|
private val dispatchers: CoroutineDispatchers,
|
||||||
) : TimelineProvider {
|
) : PinnedEventsTimelineProvider {
|
||||||
private val _timelineStateFlow: MutableStateFlow<AsyncData<Timeline>> =
|
private val _timelineStateFlow: MutableStateFlow<AsyncData<Timeline>> =
|
||||||
MutableStateFlow(AsyncData.Uninitialized)
|
MutableStateFlow(AsyncData.Uninitialized)
|
||||||
|
|
||||||
|
|
@ -18,7 +18,7 @@ import androidx.compose.runtime.rememberUpdatedState
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import dev.zacsweers.metro.Inject
|
import dev.zacsweers.metro.Inject
|
||||||
import io.element.android.features.messages.impl.pinned.PinnedEventsTimelineProvider
|
import io.element.android.features.messages.impl.pinned.DefaultPinnedEventsTimelineProvider
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.matrix.api.room.BaseRoom
|
import io.element.android.libraries.matrix.api.room.BaseRoom
|
||||||
|
|
@ -35,7 +35,7 @@ import kotlinx.coroutines.flow.onEach
|
||||||
class PinnedMessagesBannerPresenter(
|
class PinnedMessagesBannerPresenter(
|
||||||
private val room: BaseRoom,
|
private val room: BaseRoom,
|
||||||
private val itemFactory: PinnedMessagesBannerItemFactory,
|
private val itemFactory: PinnedMessagesBannerItemFactory,
|
||||||
private val pinnedEventsTimelineProvider: PinnedEventsTimelineProvider,
|
private val pinnedEventsTimelineProvider: DefaultPinnedEventsTimelineProvider,
|
||||||
) : Presenter<PinnedMessagesBannerState> {
|
) : Presenter<PinnedMessagesBannerState> {
|
||||||
private val pinnedItems = mutableStateOf<AsyncData<ImmutableList<PinnedMessagesBannerItem>>>(AsyncData.Uninitialized)
|
private val pinnedItems = mutableStateOf<AsyncData<ImmutableList<PinnedMessagesBannerItem>>>(AsyncData.Uninitialized)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ import io.element.android.features.messages.impl.UserEventPermissions
|
||||||
import io.element.android.features.messages.impl.actionlist.ActionListState
|
import io.element.android.features.messages.impl.actionlist.ActionListState
|
||||||
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
|
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
|
||||||
import io.element.android.features.messages.impl.link.LinkState
|
import io.element.android.features.messages.impl.link.LinkState
|
||||||
import io.element.android.features.messages.impl.pinned.PinnedEventsTimelineProvider
|
import io.element.android.features.messages.impl.pinned.DefaultPinnedEventsTimelineProvider
|
||||||
import io.element.android.features.messages.impl.timeline.TimelineRoomInfo
|
import io.element.android.features.messages.impl.timeline.TimelineRoomInfo
|
||||||
import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactory
|
import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactory
|
||||||
import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactoryConfig
|
import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactoryConfig
|
||||||
|
|
@ -66,7 +66,7 @@ class PinnedMessagesListPresenter(
|
||||||
@Assisted private val navigator: PinnedMessagesListNavigator,
|
@Assisted private val navigator: PinnedMessagesListNavigator,
|
||||||
private val room: JoinedRoom,
|
private val room: JoinedRoom,
|
||||||
timelineItemsFactoryCreator: TimelineItemsFactory.Creator,
|
timelineItemsFactoryCreator: TimelineItemsFactory.Creator,
|
||||||
private val timelineProvider: PinnedEventsTimelineProvider,
|
private val timelineProvider: DefaultPinnedEventsTimelineProvider,
|
||||||
private val timelineProtectionPresenter: Presenter<TimelineProtectionState>,
|
private val timelineProtectionPresenter: Presenter<TimelineProtectionState>,
|
||||||
private val linkPresenter: Presenter<LinkState>,
|
private val linkPresenter: Presenter<LinkState>,
|
||||||
private val snackbarDispatcher: SnackbarDispatcher,
|
private val snackbarDispatcher: SnackbarDispatcher,
|
||||||
|
|
|
||||||
|
|
@ -289,7 +289,7 @@ class TimelinePresenter(
|
||||||
messageShield = messageShield.value,
|
messageShield = messageShield.value,
|
||||||
resolveVerifiedUserSendFailureState = resolveVerifiedUserSendFailureState,
|
resolveVerifiedUserSendFailureState = resolveVerifiedUserSendFailureState,
|
||||||
displayThreadSummaries = displayThreadSummaries,
|
displayThreadSummaries = displayThreadSummaries,
|
||||||
eventSink = { handleEvents(it) }
|
eventSink = ::handleEvents,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -71,7 +71,7 @@ class CustomReactionPresenter(
|
||||||
target = target.value,
|
target = target.value,
|
||||||
selectedEmoji = selectedEmoji,
|
selectedEmoji = selectedEmoji,
|
||||||
recentEmojis = recentEmojis,
|
recentEmojis = recentEmojis,
|
||||||
eventSink = { handleEvents(it) }
|
eventSink = ::handleEvents,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,7 @@ class ReactionSummaryPresenter(
|
||||||
}
|
}
|
||||||
return ReactionSummaryState(
|
return ReactionSummaryState(
|
||||||
target = targetWithAvatars.value,
|
target = targetWithAvatars.value,
|
||||||
eventSink = { handleEvents(it) }
|
eventSink = ::handleEvents,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ class ReadReceiptBottomSheetPresenter : Presenter<ReadReceiptBottomSheetState> {
|
||||||
|
|
||||||
return ReadReceiptBottomSheetState(
|
return ReadReceiptBottomSheetState(
|
||||||
selectedEvent = selectedEvent,
|
selectedEvent = selectedEvent,
|
||||||
eventSink = { handleEvent(it) },
|
eventSink = ::handleEvent,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,7 @@ class TimelineProtectionPresenter(
|
||||||
|
|
||||||
return TimelineProtectionState(
|
return TimelineProtectionState(
|
||||||
protectionState = protectionState,
|
protectionState = protectionState,
|
||||||
eventSink = { event -> handleEvent(event) }
|
eventSink = ::handleEvent,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -90,7 +90,7 @@ class DefaultMessagesEntryPointTest {
|
||||||
override fun navigateToRoomDetails() = lambdaError()
|
override fun navigateToRoomDetails() = lambdaError()
|
||||||
override fun navigateToRoomMemberDetails(userId: UserId) = lambdaError()
|
override fun navigateToRoomMemberDetails(userId: UserId) = lambdaError()
|
||||||
override fun handlePermalinkClick(data: PermalinkData, pushToBackstack: Boolean) = lambdaError()
|
override fun handlePermalinkClick(data: PermalinkData, pushToBackstack: Boolean) = lambdaError()
|
||||||
override fun forwardEvent(eventId: EventId) = lambdaError()
|
override fun forwardEvent(eventId: EventId, fromPinnedEvents: Boolean) = lambdaError()
|
||||||
override fun navigateToRoom(roomId: RoomId) = lambdaError()
|
override fun navigateToRoom(roomId: RoomId) = lambdaError()
|
||||||
}
|
}
|
||||||
val initialTarget = MessagesEntryPoint.InitialTarget.Messages(focusedEventId = AN_EVENT_ID)
|
val initialTarget = MessagesEntryPoint.InitialTarget.Messages(focusedEventId = AN_EVENT_ID)
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
package io.element.android.features.messages.impl.pinned.banner
|
package io.element.android.features.messages.impl.pinned.banner
|
||||||
|
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.features.messages.impl.pinned.PinnedEventsTimelineProvider
|
import io.element.android.features.messages.impl.pinned.DefaultPinnedEventsTimelineProvider
|
||||||
import io.element.android.libraries.eventformatter.test.FakePinnedMessagesBannerFormatter
|
import io.element.android.libraries.eventformatter.test.FakePinnedMessagesBannerFormatter
|
||||||
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
||||||
import io.element.android.libraries.matrix.api.sync.SyncService
|
import io.element.android.libraries.matrix.api.sync.SyncService
|
||||||
|
|
@ -195,7 +195,7 @@ class PinnedMessagesBannerPresenterTest {
|
||||||
internal fun TestScope.createPinnedEventsTimelineProvider(
|
internal fun TestScope.createPinnedEventsTimelineProvider(
|
||||||
room: JoinedRoom = FakeJoinedRoom(),
|
room: JoinedRoom = FakeJoinedRoom(),
|
||||||
syncService: SyncService = FakeSyncService(),
|
syncService: SyncService = FakeSyncService(),
|
||||||
) = PinnedEventsTimelineProvider(
|
) = DefaultPinnedEventsTimelineProvider(
|
||||||
room = room,
|
room = room,
|
||||||
syncService = syncService,
|
syncService = syncService,
|
||||||
dispatchers = testCoroutineDispatchers(),
|
dispatchers = testCoroutineDispatchers(),
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ import io.element.android.features.messages.impl.actionlist.anActionListState
|
||||||
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
|
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
|
||||||
import io.element.android.features.messages.impl.fixtures.aTimelineItemsFactoryCreator
|
import io.element.android.features.messages.impl.fixtures.aTimelineItemsFactoryCreator
|
||||||
import io.element.android.features.messages.impl.link.aLinkState
|
import io.element.android.features.messages.impl.link.aLinkState
|
||||||
import io.element.android.features.messages.impl.pinned.PinnedEventsTimelineProvider
|
import io.element.android.features.messages.impl.pinned.DefaultPinnedEventsTimelineProvider
|
||||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||||
import io.element.android.features.messages.impl.timeline.protection.aTimelineProtectionState
|
import io.element.android.features.messages.impl.timeline.protection.aTimelineProtectionState
|
||||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
|
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
|
||||||
|
|
@ -300,7 +300,7 @@ class PinnedMessagesListPresenterTest {
|
||||||
analyticsService: AnalyticsService = FakeAnalyticsService(),
|
analyticsService: AnalyticsService = FakeAnalyticsService(),
|
||||||
featureFlagService: FakeFeatureFlagService = FakeFeatureFlagService(),
|
featureFlagService: FakeFeatureFlagService = FakeFeatureFlagService(),
|
||||||
): PinnedMessagesListPresenter {
|
): PinnedMessagesListPresenter {
|
||||||
val timelineProvider = PinnedEventsTimelineProvider(
|
val timelineProvider = DefaultPinnedEventsTimelineProvider(
|
||||||
room = room,
|
room = room,
|
||||||
syncService = syncService,
|
syncService = syncService,
|
||||||
dispatchers = testCoroutineDispatchers(),
|
dispatchers = testCoroutineDispatchers(),
|
||||||
|
|
|
||||||
|
|
@ -143,7 +143,7 @@ class EditUserProfilePresenter(
|
||||||
saveButtonEnabled = canSave && saveAction.value !is AsyncAction.Loading,
|
saveButtonEnabled = canSave && saveAction.value !is AsyncAction.Loading,
|
||||||
saveAction = saveAction.value,
|
saveAction = saveAction.value,
|
||||||
cameraPermissionState = cameraPermissionState,
|
cameraPermissionState = cameraPermissionState,
|
||||||
eventSink = { handleEvents(it) },
|
eventSink = ::handleEvents,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -165,7 +165,7 @@ class DefaultBugReporter(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val sessionData = sessionStore.getLatestSession()
|
val sessionData = sessionStore.getLatestSession()
|
||||||
val numberOfAccounts = sessionStore.getAllSessions().size
|
val numberOfAccounts = sessionStore.numberOfSessions()
|
||||||
val deviceId = sessionData?.deviceId ?: "undefined"
|
val deviceId = sessionData?.deviceId ?: "undefined"
|
||||||
val userId = sessionData?.userId?.let { UserId(it) }
|
val userId = sessionData?.userId?.let { UserId(it) }
|
||||||
// build the multi part request
|
// build the multi part request
|
||||||
|
|
|
||||||
|
|
@ -124,7 +124,7 @@ class ChangeRoomPermissionsPresenter(
|
||||||
hasChanges = hasChanges,
|
hasChanges = hasChanges,
|
||||||
saveAction = saveAction,
|
saveAction = saveAction,
|
||||||
confirmExitAction = confirmExitAction,
|
confirmExitAction = confirmExitAction,
|
||||||
eventSink = { handleEvent(it) }
|
eventSink = ::handleEvent,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -100,7 +100,7 @@ class RolesAndPermissionsPresenter(
|
||||||
canDemoteSelf = canDemoteSelf.value,
|
canDemoteSelf = canDemoteSelf.value,
|
||||||
changeOwnRoleAction = changeOwnRoleAction.value,
|
changeOwnRoleAction = changeOwnRoleAction.value,
|
||||||
resetPermissionsAction = resetPermissionsAction.value,
|
resetPermissionsAction = resetPermissionsAction.value,
|
||||||
eventSink = { handleEvent(it) },
|
eventSink = ::handleEvent,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ interface RoomDetailsEntryPoint : FeatureEntryPoint {
|
||||||
fun navigateToGlobalNotificationSettings()
|
fun navigateToGlobalNotificationSettings()
|
||||||
fun navigateToRoom(roomId: RoomId, serverNames: List<String>)
|
fun navigateToRoom(roomId: RoomId, serverNames: List<String>)
|
||||||
fun handlePermalinkClick(data: PermalinkData, pushToBackstack: Boolean)
|
fun handlePermalinkClick(data: PermalinkData, pushToBackstack: Boolean)
|
||||||
fun startForwardEventFlow(eventId: EventId)
|
fun startForwardEventFlow(eventId: EventId, fromPinnedEvents: Boolean)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createNode(
|
fun createNode(
|
||||||
|
|
|
||||||
|
|
@ -302,7 +302,7 @@ class RoomDetailsFlowNode(
|
||||||
// Cannot happen
|
// Cannot happen
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun forwardEvent(eventId: EventId) {
|
override fun forwardEvent(eventId: EventId, fromPinnedEvents: Boolean) {
|
||||||
// Cannot happen
|
// Cannot happen
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -334,8 +334,8 @@ class RoomDetailsFlowNode(
|
||||||
callback.handlePermalinkClick(permalinkData, pushToBackstack = false)
|
callback.handlePermalinkClick(permalinkData, pushToBackstack = false)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun forward(eventId: EventId) {
|
override fun forward(eventId: EventId, fromPinnedEvents: Boolean) {
|
||||||
callback.startForwardEventFlow(eventId)
|
callback.startForwardEventFlow(eventId, fromPinnedEvents)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
mediaGalleryEntryPoint.createNode(
|
mediaGalleryEntryPoint.createNode(
|
||||||
|
|
@ -361,8 +361,8 @@ class RoomDetailsFlowNode(
|
||||||
callback.handlePermalinkClick(data, pushToBackstack)
|
callback.handlePermalinkClick(data, pushToBackstack)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun forwardEvent(eventId: EventId) {
|
override fun forwardEvent(eventId: EventId, fromPinnedEvents: Boolean) {
|
||||||
callback.startForwardEventFlow(eventId)
|
callback.startForwardEventFlow(eventId, fromPinnedEvents)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun navigateToRoom(roomId: RoomId) {
|
override fun navigateToRoom(roomId: RoomId) {
|
||||||
|
|
|
||||||
|
|
@ -179,7 +179,7 @@ class RoomMemberListPresenter(
|
||||||
isSearchActive = isSearchActive,
|
isSearchActive = isSearchActive,
|
||||||
canInvite = canInvite,
|
canInvite = canInvite,
|
||||||
moderationState = roomModerationState,
|
moderationState = roomModerationState,
|
||||||
eventSink = { handleEvents(it) },
|
eventSink = ::handleEvents,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -135,7 +135,7 @@ class RoomNotificationSettingsPresenter(
|
||||||
setNotificationSettingAction = setNotificationSettingAction.value,
|
setNotificationSettingAction = setNotificationSettingAction.value,
|
||||||
restoreDefaultAction = restoreDefaultAction.value,
|
restoreDefaultAction = restoreDefaultAction.value,
|
||||||
displayMentionsOnlyDisclaimer = shouldDisplayMentionsOnlyDisclaimer,
|
displayMentionsOnlyDisclaimer = shouldDisplayMentionsOnlyDisclaimer,
|
||||||
eventSink = { handleEvents(it) },
|
eventSink = ::handleEvents,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,7 @@ class DefaultRoomDetailsEntryPointTest {
|
||||||
override fun navigateToGlobalNotificationSettings() = lambdaError()
|
override fun navigateToGlobalNotificationSettings() = lambdaError()
|
||||||
override fun navigateToRoom(roomId: RoomId, serverNames: List<String>) = lambdaError()
|
override fun navigateToRoom(roomId: RoomId, serverNames: List<String>) = lambdaError()
|
||||||
override fun handlePermalinkClick(data: PermalinkData, pushToBackstack: Boolean) = lambdaError()
|
override fun handlePermalinkClick(data: PermalinkData, pushToBackstack: Boolean) = lambdaError()
|
||||||
override fun startForwardEventFlow(eventId: EventId) = lambdaError()
|
override fun startForwardEventFlow(eventId: EventId, fromPinnedEvents: Boolean) = lambdaError()
|
||||||
}
|
}
|
||||||
val params = RoomDetailsEntryPoint.Params(
|
val params = RoomDetailsEntryPoint.Params(
|
||||||
initialElement = RoomDetailsEntryPoint.InitialTarget.RoomDetails,
|
initialElement = RoomDetailsEntryPoint.InitialTarget.RoomDetails,
|
||||||
|
|
|
||||||
|
|
@ -140,7 +140,7 @@ class RoomMemberModerationPresenter(
|
||||||
kickUserAsyncAction = kickUserAsyncAction.value,
|
kickUserAsyncAction = kickUserAsyncAction.value,
|
||||||
banUserAsyncAction = banUserAsyncAction.value,
|
banUserAsyncAction = banUserAsyncAction.value,
|
||||||
unbanUserAsyncAction = unbanUserAsyncAction.value,
|
unbanUserAsyncAction = unbanUserAsyncAction.value,
|
||||||
eventSink = { handleEvent(it) },
|
eventSink = ::handleEvent,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,7 @@ class SharePresenter(
|
||||||
|
|
||||||
return ShareState(
|
return ShareState(
|
||||||
shareAction = shareActionState.value,
|
shareAction = shareActionState.value,
|
||||||
eventSink = { handleEvents(it) }
|
eventSink = ::handleEvents,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -58,9 +58,11 @@ class SignedOutPresenterTest {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
assertThat(initialState.signedOutSession).isEqualTo(aSessionData)
|
assertThat(initialState.signedOutSession).isEqualTo(aSessionData)
|
||||||
assertThat(sessionStore.getAllSessions()).isNotEmpty()
|
assertThat(sessionStore.getAllSessions()).isNotEmpty()
|
||||||
|
assertThat(sessionStore.numberOfSessions()).isEqualTo(1)
|
||||||
initialState.eventSink(SignedOutEvents.SignInAgain)
|
initialState.eventSink(SignedOutEvents.SignInAgain)
|
||||||
assertThat(awaitItem().signedOutSession).isNull()
|
assertThat(awaitItem().signedOutSession).isNull()
|
||||||
assertThat(sessionStore.getAllSessions()).isEmpty()
|
assertThat(sessionStore.getAllSessions()).isEmpty()
|
||||||
|
assertThat(sessionStore.numberOfSessions()).isEqualTo(0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,6 @@ interface SpaceEntryPoint : FeatureEntryPoint {
|
||||||
|
|
||||||
interface Callback : Plugin {
|
interface Callback : Plugin {
|
||||||
fun navigateToRoom(roomId: RoomId, viaParameters: List<String>)
|
fun navigateToRoom(roomId: RoomId, viaParameters: List<String>)
|
||||||
fun navigateToRoomDetails()
|
|
||||||
fun navigateToRoomMemberList()
|
fun navigateToRoomMemberList()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,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 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.push
|
import com.bumble.appyx.navmodel.backstack.operation.push
|
||||||
import dev.zacsweers.metro.Assisted
|
import dev.zacsweers.metro.Assisted
|
||||||
import dev.zacsweers.metro.AssistedInject
|
import dev.zacsweers.metro.AssistedInject
|
||||||
|
|
@ -26,14 +27,15 @@ import io.element.android.features.space.api.SpaceEntryPoint
|
||||||
import io.element.android.features.space.impl.di.SpaceFlowGraph
|
import io.element.android.features.space.impl.di.SpaceFlowGraph
|
||||||
import io.element.android.features.space.impl.leave.LeaveSpaceNode
|
import io.element.android.features.space.impl.leave.LeaveSpaceNode
|
||||||
import io.element.android.features.space.impl.root.SpaceNode
|
import io.element.android.features.space.impl.root.SpaceNode
|
||||||
|
import io.element.android.features.space.impl.settings.SpaceSettingsNode
|
||||||
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.callback
|
import io.element.android.libraries.architecture.callback
|
||||||
import io.element.android.libraries.architecture.createNode
|
import io.element.android.libraries.architecture.createNode
|
||||||
import io.element.android.libraries.architecture.inputs
|
|
||||||
import io.element.android.libraries.di.DependencyInjectionGraphOwner
|
import io.element.android.libraries.di.DependencyInjectionGraphOwner
|
||||||
import io.element.android.libraries.di.RoomScope
|
import io.element.android.libraries.di.RoomScope
|
||||||
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.room.JoinedRoom
|
||||||
import io.element.android.libraries.matrix.api.spaces.SpaceService
|
import io.element.android.libraries.matrix.api.spaces.SpaceService
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
|
|
@ -42,6 +44,7 @@ import kotlinx.parcelize.Parcelize
|
||||||
class SpaceFlowNode(
|
class SpaceFlowNode(
|
||||||
@Assisted val buildContext: BuildContext,
|
@Assisted val buildContext: BuildContext,
|
||||||
@Assisted plugins: List<Plugin>,
|
@Assisted plugins: List<Plugin>,
|
||||||
|
room: JoinedRoom,
|
||||||
spaceService: SpaceService,
|
spaceService: SpaceService,
|
||||||
graphFactory: SpaceFlowGraph.Factory,
|
graphFactory: SpaceFlowGraph.Factory,
|
||||||
) : BaseFlowNode<SpaceFlowNode.NavTarget>(
|
) : BaseFlowNode<SpaceFlowNode.NavTarget>(
|
||||||
|
|
@ -52,15 +55,17 @@ class SpaceFlowNode(
|
||||||
buildContext = buildContext,
|
buildContext = buildContext,
|
||||||
plugins = plugins,
|
plugins = plugins,
|
||||||
), DependencyInjectionGraphOwner {
|
), DependencyInjectionGraphOwner {
|
||||||
private val inputs: SpaceEntryPoint.Inputs = inputs()
|
|
||||||
private val callback: SpaceEntryPoint.Callback = callback()
|
private val callback: SpaceEntryPoint.Callback = callback()
|
||||||
private val spaceRoomList = spaceService.spaceRoomList(inputs.roomId)
|
private val spaceRoomList = spaceService.spaceRoomList(room.roomId)
|
||||||
override val graph = graphFactory.create(spaceRoomList)
|
override val graph = graphFactory.create(spaceRoomList)
|
||||||
|
|
||||||
sealed interface NavTarget : Parcelable {
|
sealed interface NavTarget : Parcelable {
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data object Root : NavTarget
|
data object Root : NavTarget
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data object Settings : NavTarget
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data object Leave : NavTarget
|
data object Leave : NavTarget
|
||||||
}
|
}
|
||||||
|
|
@ -77,7 +82,16 @@ class SpaceFlowNode(
|
||||||
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||||
return when (navTarget) {
|
return when (navTarget) {
|
||||||
NavTarget.Leave -> {
|
NavTarget.Leave -> {
|
||||||
createNode<LeaveSpaceNode>(buildContext, listOf(inputs))
|
val callback = object : LeaveSpaceNode.Callback {
|
||||||
|
override fun closeLeaveSpaceFlow() {
|
||||||
|
backstack.pop()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun navigateToRolesAndPermissions() {
|
||||||
|
// TODO
|
||||||
|
}
|
||||||
|
}
|
||||||
|
createNode<LeaveSpaceNode>(buildContext, listOf(callback))
|
||||||
}
|
}
|
||||||
NavTarget.Root -> {
|
NavTarget.Root -> {
|
||||||
val callback = object : SpaceNode.Callback {
|
val callback = object : SpaceNode.Callback {
|
||||||
|
|
@ -85,8 +99,8 @@ class SpaceFlowNode(
|
||||||
callback.navigateToRoom(roomId, viaParameters)
|
callback.navigateToRoom(roomId, viaParameters)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun navigateToRoomDetails() {
|
override fun navigateToSpaceSettings() {
|
||||||
callback.navigateToRoomDetails()
|
backstack.push(NavTarget.Settings)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun navigateToRoomMemberList() {
|
override fun navigateToRoomMemberList() {
|
||||||
|
|
@ -97,7 +111,35 @@ class SpaceFlowNode(
|
||||||
backstack.push(NavTarget.Leave)
|
backstack.push(NavTarget.Leave)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
createNode<SpaceNode>(buildContext, listOf(inputs, callback))
|
createNode<SpaceNode>(buildContext, listOf(callback))
|
||||||
|
}
|
||||||
|
NavTarget.Settings -> {
|
||||||
|
val callback = object : SpaceSettingsNode.Callback {
|
||||||
|
override fun closeSettings() {
|
||||||
|
backstack.pop()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun navigateToSpaceInfo() {
|
||||||
|
// TODO
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun navigateToSpaceMembers() {
|
||||||
|
callback.navigateToRoomMemberList()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun navigateToRolesAndPermissions() {
|
||||||
|
// TODO
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun navigateToSecurityAndPrivacy() {
|
||||||
|
// TODO
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun startLeaveSpaceFlow() {
|
||||||
|
backstack.push(NavTarget.Leave)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
createNode<SpaceSettingsNode>(buildContext, listOf(callback))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,10 +16,10 @@ import com.bumble.appyx.core.plugin.Plugin
|
||||||
import dev.zacsweers.metro.Assisted
|
import dev.zacsweers.metro.Assisted
|
||||||
import dev.zacsweers.metro.AssistedInject
|
import dev.zacsweers.metro.AssistedInject
|
||||||
import io.element.android.annotations.ContributesNode
|
import io.element.android.annotations.ContributesNode
|
||||||
import io.element.android.features.space.api.SpaceEntryPoint
|
|
||||||
import io.element.android.features.space.impl.di.SpaceFlowScope
|
import io.element.android.features.space.impl.di.SpaceFlowScope
|
||||||
import io.element.android.libraries.architecture.inputs
|
import io.element.android.libraries.architecture.callback
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
|
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
||||||
|
|
||||||
@ContributesNode(SpaceFlowScope::class)
|
@ContributesNode(SpaceFlowScope::class)
|
||||||
@AssistedInject
|
@AssistedInject
|
||||||
|
|
@ -27,12 +27,19 @@ class LeaveSpaceNode(
|
||||||
@Assisted buildContext: BuildContext,
|
@Assisted buildContext: BuildContext,
|
||||||
@Assisted plugins: List<Plugin>,
|
@Assisted plugins: List<Plugin>,
|
||||||
matrixClient: MatrixClient,
|
matrixClient: MatrixClient,
|
||||||
|
room: JoinedRoom,
|
||||||
presenterFactory: LeaveSpacePresenter.Factory,
|
presenterFactory: LeaveSpacePresenter.Factory,
|
||||||
) : Node(buildContext, plugins = plugins) {
|
) : Node(buildContext, plugins = plugins) {
|
||||||
private val inputs: SpaceEntryPoint.Inputs = inputs()
|
interface Callback : Plugin {
|
||||||
private val leaveSpaceHandle = matrixClient.spaceService.getLeaveSpaceHandle(inputs.roomId)
|
fun closeLeaveSpaceFlow()
|
||||||
|
fun navigateToRolesAndPermissions()
|
||||||
|
}
|
||||||
|
|
||||||
|
private val leaveSpaceHandle = matrixClient.spaceService.getLeaveSpaceHandle(room.roomId)
|
||||||
private val presenter: LeaveSpacePresenter = presenterFactory.create(leaveSpaceHandle)
|
private val presenter: LeaveSpacePresenter = presenterFactory.create(leaveSpaceHandle)
|
||||||
|
|
||||||
|
private val callback: Callback = callback()
|
||||||
|
|
||||||
override fun onBuilt() {
|
override fun onBuilt() {
|
||||||
super.onBuilt()
|
super.onBuilt()
|
||||||
lifecycle.subscribe(
|
lifecycle.subscribe(
|
||||||
|
|
@ -47,7 +54,8 @@ class LeaveSpaceNode(
|
||||||
val state = presenter.present()
|
val state = presenter.present()
|
||||||
LeaveSpaceView(
|
LeaveSpaceView(
|
||||||
state = state,
|
state = state,
|
||||||
onCancel = ::navigateUp,
|
onCancel = callback::closeLeaveSpaceFlow,
|
||||||
|
onRolesAndPermissionsClick = callback::navigateToRolesAndPermissions,
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -69,6 +69,7 @@ import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
fun LeaveSpaceView(
|
fun LeaveSpaceView(
|
||||||
state: LeaveSpaceState,
|
state: LeaveSpaceState,
|
||||||
onCancel: () -> Unit,
|
onCancel: () -> Unit,
|
||||||
|
onRolesAndPermissionsClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
Scaffold(
|
Scaffold(
|
||||||
|
|
@ -130,6 +131,9 @@ fun LeaveSpaceView(
|
||||||
state.eventSink(LeaveSpaceEvents.LeaveSpace)
|
state.eventSink(LeaveSpaceEvents.LeaveSpace)
|
||||||
},
|
},
|
||||||
onCancel = onCancel,
|
onCancel = onCancel,
|
||||||
|
// TODO enable when navigation is ready
|
||||||
|
showRolesAndPermissionsButton = false, // state.isLastAdmin,
|
||||||
|
onRolesAndPermissionsClick = onRolesAndPermissionsClick,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -210,6 +214,8 @@ private fun LeaveSpaceButtons(
|
||||||
showLeaveButton: Boolean,
|
showLeaveButton: Boolean,
|
||||||
selectedRoomsCount: Int,
|
selectedRoomsCount: Int,
|
||||||
onLeaveSpace: () -> Unit,
|
onLeaveSpace: () -> Unit,
|
||||||
|
showRolesAndPermissionsButton: Boolean,
|
||||||
|
onRolesAndPermissionsClick: () -> Unit,
|
||||||
onCancel: () -> Unit,
|
onCancel: () -> Unit,
|
||||||
) {
|
) {
|
||||||
ButtonColumnMolecule(
|
ButtonColumnMolecule(
|
||||||
|
|
@ -229,8 +235,14 @@ private fun LeaveSpaceButtons(
|
||||||
destructive = true,
|
destructive = true,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
// TODO For least admin space, add a button to open the settings.
|
if (showRolesAndPermissionsButton) {
|
||||||
// See https://www.figma.com/design/kcnHxunG1LDWXsJhaNuiHz/ER-145--Spaces-on-Element-X?node-id=4622-59600
|
Button(
|
||||||
|
text = stringResource(CommonStrings.action_go_to_roles_and_permissions),
|
||||||
|
onClick = onRolesAndPermissionsClick,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
leadingIcon = IconSource.Vector(CompoundIcons.Settings()),
|
||||||
|
)
|
||||||
|
}
|
||||||
TextButton(
|
TextButton(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
text = stringResource(CommonStrings.action_cancel),
|
text = stringResource(CommonStrings.action_cancel),
|
||||||
|
|
@ -345,5 +357,6 @@ internal fun LeaveSpaceViewPreview(
|
||||||
LeaveSpaceView(
|
LeaveSpaceView(
|
||||||
state = state,
|
state = state,
|
||||||
onCancel = {},
|
onCancel = {},
|
||||||
|
onRolesAndPermissionsClick = {},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ class SpaceNode(
|
||||||
) : Node(buildContext, plugins = plugins) {
|
) : Node(buildContext, plugins = plugins) {
|
||||||
interface Callback : Plugin {
|
interface Callback : Plugin {
|
||||||
fun navigateToRoom(roomId: RoomId, viaParameters: List<String>)
|
fun navigateToRoom(roomId: RoomId, viaParameters: List<String>)
|
||||||
fun navigateToRoomDetails()
|
fun navigateToSpaceSettings()
|
||||||
fun navigateToRoomMemberList()
|
fun navigateToRoomMemberList()
|
||||||
fun startLeaveSpaceFlow()
|
fun startLeaveSpaceFlow()
|
||||||
}
|
}
|
||||||
|
|
@ -80,7 +80,7 @@ class SpaceNode(
|
||||||
callback.navigateToRoom(spaceRoom.roomId, spaceRoom.via)
|
callback.navigateToRoom(spaceRoom.roomId, spaceRoom.via)
|
||||||
},
|
},
|
||||||
onDetailsClick = {
|
onDetailsClick = {
|
||||||
callback.navigateToRoomDetails()
|
callback.navigateToSpaceSettings()
|
||||||
},
|
},
|
||||||
onShareSpace = {
|
onShareSpace = {
|
||||||
onShareRoom(context)
|
onShareRoom(context)
|
||||||
|
|
|
||||||
|
|
@ -328,7 +328,7 @@ private fun SpaceViewTopBar(
|
||||||
},
|
},
|
||||||
text = {
|
text = {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(id = CommonStrings.action_leave),
|
text = stringResource(id = CommonStrings.action_leave_space),
|
||||||
color = ElementTheme.colors.textCriticalPrimary,
|
color = ElementTheme.colors.textCriticalPrimary,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
/*
|
||||||
|
* 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.space.impl.settings
|
||||||
|
|
||||||
|
sealed interface SpaceSettingsEvents
|
||||||
|
|
@ -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.space.impl.settings
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import com.bumble.appyx.core.modality.BuildContext
|
||||||
|
import com.bumble.appyx.core.node.Node
|
||||||
|
import com.bumble.appyx.core.plugin.Plugin
|
||||||
|
import dev.zacsweers.metro.Assisted
|
||||||
|
import dev.zacsweers.metro.AssistedInject
|
||||||
|
import io.element.android.annotations.ContributesNode
|
||||||
|
import io.element.android.features.space.impl.di.SpaceFlowScope
|
||||||
|
import io.element.android.libraries.architecture.appyx.launchMolecule
|
||||||
|
import io.element.android.libraries.architecture.callback
|
||||||
|
|
||||||
|
@ContributesNode(SpaceFlowScope::class)
|
||||||
|
@AssistedInject
|
||||||
|
class SpaceSettingsNode(
|
||||||
|
@Assisted buildContext: BuildContext,
|
||||||
|
@Assisted plugins: List<Plugin>,
|
||||||
|
private val presenter: SpaceSettingsPresenter,
|
||||||
|
) : Node(buildContext, plugins = plugins) {
|
||||||
|
interface Callback : Plugin {
|
||||||
|
fun closeSettings()
|
||||||
|
|
||||||
|
fun navigateToSpaceInfo()
|
||||||
|
fun navigateToSpaceMembers()
|
||||||
|
fun navigateToRolesAndPermissions()
|
||||||
|
fun navigateToSecurityAndPrivacy()
|
||||||
|
fun startLeaveSpaceFlow()
|
||||||
|
}
|
||||||
|
|
||||||
|
private val callback: Callback = callback()
|
||||||
|
private val stateFlow = launchMolecule { presenter.present() }
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun View(modifier: Modifier) {
|
||||||
|
val state by stateFlow.collectAsState()
|
||||||
|
SpaceSettingsView(
|
||||||
|
state = state,
|
||||||
|
modifier = modifier,
|
||||||
|
onSpaceInfoClick = callback::navigateToSpaceInfo,
|
||||||
|
onBackClick = callback::closeSettings,
|
||||||
|
onMembersClick = callback::navigateToSpaceMembers,
|
||||||
|
onRolesAndPermissionsClick = callback::navigateToRolesAndPermissions,
|
||||||
|
onSecurityAndPrivacyClick = callback::navigateToSecurityAndPrivacy,
|
||||||
|
onLeaveSpaceClick = callback::startLeaveSpaceFlow,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
/*
|
||||||
|
* 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.space.impl.settings
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import dev.zacsweers.metro.Inject
|
||||||
|
import io.element.android.libraries.architecture.Presenter
|
||||||
|
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
||||||
|
import io.element.android.libraries.matrix.ui.room.isOwnUserAdmin
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
class SpaceSettingsPresenter(
|
||||||
|
private val room: JoinedRoom,
|
||||||
|
) : Presenter<SpaceSettingsState> {
|
||||||
|
@Composable
|
||||||
|
override fun present(): SpaceSettingsState {
|
||||||
|
val roomInfo by room.roomInfoFlow.collectAsState()
|
||||||
|
val isUserAdmin = room.isOwnUserAdmin()
|
||||||
|
return SpaceSettingsState(
|
||||||
|
roomId = room.roomId,
|
||||||
|
name = roomInfo.name.orEmpty(),
|
||||||
|
canonicalAlias = roomInfo.canonicalAlias,
|
||||||
|
avatarUrl = roomInfo.avatarUrl,
|
||||||
|
memberCount = roomInfo.activeMembersCount,
|
||||||
|
showRolesAndPermissions = isUserAdmin,
|
||||||
|
showSecurityAndPrivacy = isUserAdmin,
|
||||||
|
eventSink = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
/*
|
||||||
|
* 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.space.impl.settings
|
||||||
|
|
||||||
|
import io.element.android.libraries.matrix.api.core.RoomAlias
|
||||||
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
|
||||||
|
data class SpaceSettingsState(
|
||||||
|
val roomId: RoomId,
|
||||||
|
val name: String,
|
||||||
|
val canonicalAlias: RoomAlias?,
|
||||||
|
val avatarUrl: String?,
|
||||||
|
val memberCount: Long,
|
||||||
|
val showRolesAndPermissions: Boolean,
|
||||||
|
val showSecurityAndPrivacy: Boolean,
|
||||||
|
val eventSink: (SpaceSettingsEvents) -> Unit
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
/*
|
||||||
|
* 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.space.impl.settings
|
||||||
|
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
|
import io.element.android.libraries.matrix.api.core.RoomAlias
|
||||||
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
|
||||||
|
open class SpaceSettingsStateProvider : PreviewParameterProvider<SpaceSettingsState> {
|
||||||
|
override val values: Sequence<SpaceSettingsState>
|
||||||
|
get() = sequenceOf(
|
||||||
|
aSpaceSettingsState(),
|
||||||
|
aSpaceSettingsState(alias = null),
|
||||||
|
aSpaceSettingsState(showSecurityAndPrivacy = true),
|
||||||
|
aSpaceSettingsState(showRolesAndPermissions = true),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun aSpaceSettingsState(
|
||||||
|
roomId: RoomId = RoomId("!aRoomId:element.io"),
|
||||||
|
name: String = "Space name",
|
||||||
|
alias: RoomAlias? = RoomAlias("#spacename:element.io"),
|
||||||
|
avatarUrl: String? = null,
|
||||||
|
memberCount: Long = 100,
|
||||||
|
showRolesAndPermissions: Boolean = false,
|
||||||
|
showSecurityAndPrivacy: Boolean = false,
|
||||||
|
eventSink: (SpaceSettingsEvents) -> Unit = {},
|
||||||
|
) = SpaceSettingsState(
|
||||||
|
roomId = roomId,
|
||||||
|
name = name,
|
||||||
|
canonicalAlias = alias,
|
||||||
|
avatarUrl = avatarUrl,
|
||||||
|
memberCount = memberCount,
|
||||||
|
showRolesAndPermissions = showRolesAndPermissions,
|
||||||
|
showSecurityAndPrivacy = showSecurityAndPrivacy,
|
||||||
|
eventSink = eventSink,
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,231 @@
|
||||||
|
/*
|
||||||
|
* 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.space.impl.settings
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
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.space.impl.R
|
||||||
|
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
||||||
|
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||||
|
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||||
|
import io.element.android.libraries.designsystem.components.avatar.AvatarType
|
||||||
|
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||||
|
import io.element.android.libraries.designsystem.components.list.ListItemContent
|
||||||
|
import io.element.android.libraries.designsystem.components.preferences.PreferenceCategory
|
||||||
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.IconSource
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.ListItem
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.ListItemStyle
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||||
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SpaceSettingsView(
|
||||||
|
state: SpaceSettingsState,
|
||||||
|
onBackClick: () -> Unit,
|
||||||
|
onSpaceInfoClick: () -> Unit,
|
||||||
|
onMembersClick: () -> Unit,
|
||||||
|
onRolesAndPermissionsClick: () -> Unit,
|
||||||
|
onSecurityAndPrivacyClick: () -> Unit,
|
||||||
|
onLeaveSpaceClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
Scaffold(
|
||||||
|
modifier = modifier,
|
||||||
|
topBar = {
|
||||||
|
SpaceSettingsTopBar(onBackClick = onBackClick)
|
||||||
|
},
|
||||||
|
) { padding ->
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(padding)
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
) {
|
||||||
|
SpaceInfoSection(
|
||||||
|
roomId = state.roomId,
|
||||||
|
name = state.name,
|
||||||
|
avatarUrl = state.avatarUrl,
|
||||||
|
canonicalAlias = state.canonicalAlias?.value,
|
||||||
|
onSpaceInfoClick = onSpaceInfoClick,
|
||||||
|
)
|
||||||
|
Section(isVisible = state.showSecurityAndPrivacy, content = {
|
||||||
|
SecurityAndPrivacyItem(
|
||||||
|
onClick = onSecurityAndPrivacyClick
|
||||||
|
)
|
||||||
|
})
|
||||||
|
Section(content = {
|
||||||
|
MembersItem(state.memberCount, onClick = onMembersClick)
|
||||||
|
if (state.showRolesAndPermissions) {
|
||||||
|
RolesAndPermissionsItem(onClick = onRolesAndPermissionsClick)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
Section(content = {
|
||||||
|
LeaveSpaceItem(
|
||||||
|
onClick = onLeaveSpaceClick
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun SpaceInfoSection(
|
||||||
|
roomId: RoomId,
|
||||||
|
name: String,
|
||||||
|
avatarUrl: String?,
|
||||||
|
canonicalAlias: String?,
|
||||||
|
onSpaceInfoClick: () -> Unit,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable(onClick = onSpaceInfoClick)
|
||||||
|
.padding(16.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Avatar(
|
||||||
|
avatarData = AvatarData(roomId.value, name, avatarUrl, AvatarSize.SpaceListItem),
|
||||||
|
avatarType = AvatarType.Space(),
|
||||||
|
contentDescription = avatarUrl?.let { stringResource(CommonStrings.a11y_avatar) },
|
||||||
|
)
|
||||||
|
Spacer(Modifier.width(16.dp))
|
||||||
|
Column {
|
||||||
|
Text(
|
||||||
|
text = name,
|
||||||
|
style = ElementTheme.typography.fontHeadingMdRegular,
|
||||||
|
color = ElementTheme.colors.textPrimary,
|
||||||
|
)
|
||||||
|
if (canonicalAlias != null) {
|
||||||
|
Text(
|
||||||
|
text = canonicalAlias,
|
||||||
|
style = ElementTheme.typography.fontBodyMdRegular,
|
||||||
|
color = ElementTheme.colors.textSecondary,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun Section(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
isVisible: Boolean = true,
|
||||||
|
content: @Composable ColumnScope.() -> Unit,
|
||||||
|
) {
|
||||||
|
if (isVisible) {
|
||||||
|
PreferenceCategory(content = content, modifier = modifier)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
private fun SpaceSettingsTopBar(
|
||||||
|
onBackClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
TopAppBar(
|
||||||
|
titleStr = stringResource(CommonStrings.common_settings),
|
||||||
|
navigationIcon = { BackButton(onClick = onBackClick) },
|
||||||
|
modifier = modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun SecurityAndPrivacyItem(
|
||||||
|
onClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
ListItem(
|
||||||
|
headlineContent = { Text(stringResource(R.string.screen_space_settings_security_and_privacy)) },
|
||||||
|
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Lock())),
|
||||||
|
onClick = onClick,
|
||||||
|
modifier = modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun MembersItem(
|
||||||
|
memberCount: Long,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
ListItem(
|
||||||
|
headlineContent = { Text(stringResource(CommonStrings.common_people)) },
|
||||||
|
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.User())),
|
||||||
|
trailingContent = ListItemContent.Text(memberCount.toString()),
|
||||||
|
onClick = onClick,
|
||||||
|
modifier = modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun RolesAndPermissionsItem(
|
||||||
|
onClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
ListItem(
|
||||||
|
headlineContent = { Text(stringResource(R.string.screen_space_settings_roles_and_permissions)) },
|
||||||
|
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Admin())),
|
||||||
|
onClick = onClick,
|
||||||
|
modifier = modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun LeaveSpaceItem(
|
||||||
|
onClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
ListItem(
|
||||||
|
headlineContent = {
|
||||||
|
Text(stringResource(CommonStrings.action_leave_space))
|
||||||
|
},
|
||||||
|
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Leave())),
|
||||||
|
style = ListItemStyle.Destructive,
|
||||||
|
onClick = onClick,
|
||||||
|
modifier = modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreviewsDayNight
|
||||||
|
@Composable
|
||||||
|
internal fun SpaceSettingsViewPreview(
|
||||||
|
@PreviewParameter(SpaceSettingsStateProvider::class) state: SpaceSettingsState
|
||||||
|
) = ElementPreview {
|
||||||
|
SpaceSettingsView(
|
||||||
|
state = state,
|
||||||
|
onBackClick = {},
|
||||||
|
onSpaceInfoClick = {},
|
||||||
|
onMembersClick = {},
|
||||||
|
onRolesAndPermissionsClick = {},
|
||||||
|
onSecurityAndPrivacyClick = {},
|
||||||
|
onLeaveSpaceClick = {},
|
||||||
|
modifier = Modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -10,4 +10,7 @@
|
||||||
<string name="screen_leave_space_subtitle_only_last_admin">"You will not be removed from the following room(s) because you\'re the only administrator:"</string>
|
<string name="screen_leave_space_subtitle_only_last_admin">"You will not be removed from the following room(s) because you\'re the only administrator:"</string>
|
||||||
<string name="screen_leave_space_title">"Leave %1$s?"</string>
|
<string name="screen_leave_space_title">"Leave %1$s?"</string>
|
||||||
<string name="screen_leave_space_title_last_admin">"You are the only admin for %1$s"</string>
|
<string name="screen_leave_space_title_last_admin">"You are the only admin for %1$s"</string>
|
||||||
|
<string name="screen_space_settings_leave_space">"Leave space"</string>
|
||||||
|
<string name="screen_space_settings_roles_and_permissions">"Roles & permissions"</string>
|
||||||
|
<string name="screen_space_settings_security_and_privacy">"Security & privacy"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ import io.element.android.features.space.api.SpaceEntryPoint
|
||||||
import io.element.android.features.space.impl.di.FakeSpaceFlowGraph
|
import io.element.android.features.space.impl.di.FakeSpaceFlowGraph
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||||
|
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
|
||||||
import io.element.android.libraries.matrix.test.spaces.FakeSpaceRoomList
|
import io.element.android.libraries.matrix.test.spaces.FakeSpaceRoomList
|
||||||
import io.element.android.libraries.matrix.test.spaces.FakeSpaceService
|
import io.element.android.libraries.matrix.test.spaces.FakeSpaceService
|
||||||
import io.element.android.tests.testutils.lambda.lambdaError
|
import io.element.android.tests.testutils.lambda.lambdaError
|
||||||
|
|
@ -40,12 +41,12 @@ class DefaultSpaceEntryPointTest {
|
||||||
spaceService = FakeSpaceService(
|
spaceService = FakeSpaceService(
|
||||||
spaceRoomListResult = { _: RoomId -> FakeSpaceRoomList(A_ROOM_ID) }
|
spaceRoomListResult = { _: RoomId -> FakeSpaceRoomList(A_ROOM_ID) }
|
||||||
),
|
),
|
||||||
|
room = FakeJoinedRoom(),
|
||||||
graphFactory = FakeSpaceFlowGraph.Factory
|
graphFactory = FakeSpaceFlowGraph.Factory
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
val callback = object : SpaceEntryPoint.Callback {
|
val callback = object : SpaceEntryPoint.Callback {
|
||||||
override fun navigateToRoom(roomId: RoomId, viaParameters: List<String>) = lambdaError()
|
override fun navigateToRoom(roomId: RoomId, viaParameters: List<String>) = lambdaError()
|
||||||
override fun navigateToRoomDetails() = lambdaError()
|
|
||||||
override fun navigateToRoomMemberList() = lambdaError()
|
override fun navigateToRoomMemberList() = lambdaError()
|
||||||
}
|
}
|
||||||
val result = entryPoint.createNode(
|
val result = entryPoint.createNode(
|
||||||
|
|
|
||||||
|
|
@ -103,7 +103,7 @@ class UserProfileFlowNode(
|
||||||
// Cannot happen
|
// Cannot happen
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun forwardEvent(eventId: EventId) {
|
override fun forwardEvent(eventId: EventId, fromPinnedEvents: Boolean) {
|
||||||
// Cannot happen
|
// Cannot happen
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ accompanist = "0.37.3"
|
||||||
|
|
||||||
# Test
|
# Test
|
||||||
test_core = "1.7.0"
|
test_core = "1.7.0"
|
||||||
roborazzi = "1.50.0"
|
roborazzi = "1.51.0"
|
||||||
|
|
||||||
# Jetbrain
|
# Jetbrain
|
||||||
datetime = "0.7.1"
|
datetime = "0.7.1"
|
||||||
|
|
@ -52,7 +52,7 @@ haze = "1.6.10"
|
||||||
dependencyAnalysis = "3.4.1"
|
dependencyAnalysis = "3.4.1"
|
||||||
|
|
||||||
# DI
|
# DI
|
||||||
metro = "0.7.3"
|
metro = "0.7.4"
|
||||||
|
|
||||||
# Auto service
|
# Auto service
|
||||||
autoservice = "1.1.1"
|
autoservice = "1.1.1"
|
||||||
|
|
@ -177,7 +177,7 @@ test_detekt_test = { module = "io.gitlab.arturbosch.detekt:detekt-test", version
|
||||||
# https://github.com/matrix-org/matrix-rust-components-kotlin/commits/main/sdk/sdk-android/src/main/kotlin/org/matrix/rustcomponents/sdk/matrix_sdk_ffi.kt
|
# https://github.com/matrix-org/matrix-rust-components-kotlin/commits/main/sdk/sdk-android/src/main/kotlin/org/matrix/rustcomponents/sdk/matrix_sdk_ffi.kt
|
||||||
# All new features should not be implemented in the pull request that upgrades the version, developers should
|
# All new features should not be implemented in the pull request that upgrades the version, developers should
|
||||||
# only fix API breaks and may add some TODOs.
|
# only fix API breaks and may add some TODOs.
|
||||||
matrix_sdk = "org.matrix.rustcomponents:sdk-android:25.10.31"
|
matrix_sdk = "org.matrix.rustcomponents:sdk-android:25.11.4"
|
||||||
|
|
||||||
# Others
|
# Others
|
||||||
coil = { module = "io.coil-kt.coil3:coil", version.ref = "coil" }
|
coil = { module = "io.coil-kt.coil3:coil", version.ref = "coil" }
|
||||||
|
|
@ -229,7 +229,7 @@ sigpwned_emoji4j = "com.sigpwned:emoji4j-core:16.0.0"
|
||||||
metro_runtime = { module = "dev.zacsweers.metro:runtime", version.ref = "metro" }
|
metro_runtime = { module = "dev.zacsweers.metro:runtime", version.ref = "metro" }
|
||||||
|
|
||||||
# Element Call
|
# Element Call
|
||||||
element_call_embedded = "io.element.android:element-call-embedded:0.16.0"
|
element_call_embedded = "io.element.android:element-call-embedded:0.16.1"
|
||||||
|
|
||||||
# Auto services
|
# Auto services
|
||||||
google_autoservice = { module = "com.google.auto.service:auto-service", version.ref = "autoservice" }
|
google_autoservice = { module = "com.google.auto.service:auto-service", version.ref = "autoservice" }
|
||||||
|
|
|
||||||
|
|
@ -18,12 +18,12 @@ android {
|
||||||
testOptions {
|
testOptions {
|
||||||
unitTests.isIncludeAndroidResources = true
|
unitTests.isIncludeAndroidResources = true
|
||||||
}
|
}
|
||||||
|
}
|
||||||
dependencies {
|
|
||||||
implementation(libs.showkase)
|
dependencies {
|
||||||
testCommonDependencies(libs)
|
implementation(libs.showkase)
|
||||||
testImplementation(libs.test.roborazzi)
|
testCommonDependencies(libs)
|
||||||
testImplementation(libs.test.roborazzi.compose)
|
testImplementation(libs.test.roborazzi)
|
||||||
testImplementation(libs.test.roborazzi.junit)
|
testImplementation(libs.test.roborazzi.compose)
|
||||||
}
|
testImplementation(libs.test.roborazzi.junit)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,8 @@ plugins {
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "io.element.android.libraries.cryptography.test"
|
namespace = "io.element.android.libraries.cryptography.test"
|
||||||
|
}
|
||||||
dependencies {
|
|
||||||
api(projects.libraries.cryptography.api)
|
dependencies {
|
||||||
}
|
api(projects.libraries.cryptography.api)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,8 @@ plugins {
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "io.element.android.libraries.dateformatter.api"
|
namespace = "io.element.android.libraries.dateformatter.api"
|
||||||
|
}
|
||||||
dependencies {
|
|
||||||
testCommonDependencies(libs)
|
dependencies {
|
||||||
}
|
testCommonDependencies(libs)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,19 +30,19 @@ android {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
dependencies {
|
|
||||||
implementation(projects.libraries.core)
|
dependencies {
|
||||||
implementation(projects.libraries.designsystem)
|
implementation(projects.libraries.core)
|
||||||
implementation(projects.libraries.di)
|
implementation(projects.libraries.designsystem)
|
||||||
implementation(projects.libraries.uiStrings)
|
implementation(projects.libraries.di)
|
||||||
implementation(projects.services.toolbox.api)
|
implementation(projects.libraries.uiStrings)
|
||||||
|
implementation(projects.services.toolbox.api)
|
||||||
api(projects.libraries.dateformatter.api)
|
|
||||||
api(libs.datetime)
|
api(projects.libraries.dateformatter.api)
|
||||||
|
api(libs.datetime)
|
||||||
testCommonDependencies(libs, true)
|
|
||||||
testImplementation(projects.libraries.dateformatter.test)
|
testCommonDependencies(libs, true)
|
||||||
testImplementation(projects.services.toolbox.test)
|
testImplementation(projects.libraries.dateformatter.test)
|
||||||
}
|
testImplementation(projects.services.toolbox.test)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,9 +11,9 @@ plugins {
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "io.element.android.libraries.dateformatter.test"
|
namespace = "io.element.android.libraries.dateformatter.test"
|
||||||
|
}
|
||||||
dependencies {
|
|
||||||
api(projects.libraries.dateformatter.api)
|
dependencies {
|
||||||
api(libs.datetime)
|
api(projects.libraries.dateformatter.api)
|
||||||
}
|
api(libs.datetime)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,24 +25,24 @@ android {
|
||||||
consumerProguardFiles("consumer-rules.pro")
|
consumerProguardFiles("consumer-rules.pro")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
dependencies {
|
|
||||||
api(projects.libraries.compound)
|
dependencies {
|
||||||
|
api(projects.libraries.compound)
|
||||||
implementation(libs.androidx.compose.material3.windowsizeclass)
|
|
||||||
implementation(libs.androidx.compose.material3.adaptive)
|
implementation(libs.androidx.compose.material3.windowsizeclass)
|
||||||
implementation(libs.coil.compose)
|
implementation(libs.androidx.compose.material3.adaptive)
|
||||||
implementation(libs.vanniktech.blurhash)
|
implementation(libs.coil.compose)
|
||||||
implementation(projects.libraries.androidutils)
|
implementation(libs.vanniktech.blurhash)
|
||||||
implementation(projects.libraries.architecture)
|
implementation(projects.libraries.androidutils)
|
||||||
implementation(projects.libraries.core)
|
implementation(projects.libraries.architecture)
|
||||||
implementation(projects.libraries.preferences.api)
|
implementation(projects.libraries.core)
|
||||||
implementation(projects.libraries.testtags)
|
implementation(projects.libraries.preferences.api)
|
||||||
implementation(projects.libraries.uiStrings)
|
implementation(projects.libraries.testtags)
|
||||||
|
implementation(projects.libraries.uiStrings)
|
||||||
ksp(libs.showkase.processor)
|
|
||||||
implementation(libs.showkase)
|
ksp(libs.showkase.processor)
|
||||||
|
implementation(libs.showkase)
|
||||||
testCommonDependencies(libs)
|
|
||||||
}
|
testCommonDependencies(libs)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,11 +11,11 @@ plugins {
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "io.element.android.libraries.featureflag.test"
|
namespace = "io.element.android.libraries.featureflag.test"
|
||||||
|
}
|
||||||
dependencies {
|
|
||||||
api(projects.libraries.featureflag.api)
|
dependencies {
|
||||||
implementation(projects.libraries.core)
|
api(projects.libraries.featureflag.api)
|
||||||
implementation(projects.libraries.matrix.test)
|
implementation(projects.libraries.core)
|
||||||
implementation(libs.coroutines.core)
|
implementation(projects.libraries.matrix.test)
|
||||||
}
|
implementation(libs.coroutines.core)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.matrix.api.auth
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks the homeserver's compatibility with Element X.
|
||||||
|
*/
|
||||||
|
interface HomeServerLoginCompatibilityChecker {
|
||||||
|
/**
|
||||||
|
* Performs the compatibility check given the homeserver's [url].
|
||||||
|
* @return a `true` value if the homeserver is compatible, `false` if not, or a failure result if the check unexpectedly failed.
|
||||||
|
*/
|
||||||
|
suspend fun check(url: String): Result<Boolean>
|
||||||
|
}
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
|
|
||||||
package io.element.android.libraries.matrix.api.encryption
|
package io.element.android.libraries.matrix.api.encryption
|
||||||
|
|
||||||
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
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.encryption.identity.IdentityState
|
import io.element.android.libraries.matrix.api.encryption.identity.IdentityState
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
@ -17,7 +18,7 @@ interface EncryptionService {
|
||||||
val recoveryStateStateFlow: StateFlow<RecoveryState>
|
val recoveryStateStateFlow: StateFlow<RecoveryState>
|
||||||
val enableRecoveryProgressStateFlow: StateFlow<EnableRecoveryProgress>
|
val enableRecoveryProgressStateFlow: StateFlow<EnableRecoveryProgress>
|
||||||
val isLastDevice: StateFlow<Boolean>
|
val isLastDevice: StateFlow<Boolean>
|
||||||
val hasDevicesToVerifyAgainst: StateFlow<Boolean>
|
val hasDevicesToVerifyAgainst: StateFlow<AsyncData<Boolean>>
|
||||||
|
|
||||||
suspend fun enableBackups(): Result<Unit>
|
suspend fun enableBackups(): Result<Unit>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.libraries.matrix.impl.auth
|
||||||
|
|
||||||
|
import dev.zacsweers.metro.AppScope
|
||||||
|
import dev.zacsweers.metro.ContributesBinding
|
||||||
|
import dev.zacsweers.metro.Inject
|
||||||
|
import io.element.android.libraries.core.extensions.runCatchingExceptions
|
||||||
|
import io.element.android.libraries.matrix.api.auth.HomeServerLoginCompatibilityChecker
|
||||||
|
import io.element.android.libraries.matrix.impl.ClientBuilderProvider
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
|
@ContributesBinding(AppScope::class)
|
||||||
|
@Inject
|
||||||
|
class RustHomeServerLoginCompatibilityChecker(
|
||||||
|
private val clientBuilderProvider: ClientBuilderProvider,
|
||||||
|
) : HomeServerLoginCompatibilityChecker {
|
||||||
|
override suspend fun check(url: String): Result<Boolean> = runCatchingExceptions {
|
||||||
|
clientBuilderProvider.provide()
|
||||||
|
.inMemoryStore()
|
||||||
|
.serverNameOrHomeserverUrl(url)
|
||||||
|
.build()
|
||||||
|
.use {
|
||||||
|
it.homeserverLoginDetails()
|
||||||
|
}
|
||||||
|
.use {
|
||||||
|
Timber.d("Homeserver $url | OIDC: ${it.supportsOidcLogin()} | Password: ${it.supportsPasswordLogin()} | SSO: ${it.supportsSsoLogin()}")
|
||||||
|
it.supportsOidcLogin() || it.supportsPasswordLogin()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
|
|
||||||
package io.element.android.libraries.matrix.impl.encryption
|
package io.element.android.libraries.matrix.impl.encryption
|
||||||
|
|
||||||
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||||
import io.element.android.libraries.core.extensions.flatMap
|
import io.element.android.libraries.core.extensions.flatMap
|
||||||
import io.element.android.libraries.core.extensions.mapFailure
|
import io.element.android.libraries.core.extensions.mapFailure
|
||||||
|
|
@ -42,6 +43,7 @@ import org.matrix.rustcomponents.sdk.Client
|
||||||
import org.matrix.rustcomponents.sdk.EnableRecoveryProgressListener
|
import org.matrix.rustcomponents.sdk.EnableRecoveryProgressListener
|
||||||
import org.matrix.rustcomponents.sdk.Encryption
|
import org.matrix.rustcomponents.sdk.Encryption
|
||||||
import org.matrix.rustcomponents.sdk.UserIdentity
|
import org.matrix.rustcomponents.sdk.UserIdentity
|
||||||
|
import timber.log.Timber
|
||||||
import org.matrix.rustcomponents.sdk.BackupUploadState as RustBackupUploadState
|
import org.matrix.rustcomponents.sdk.BackupUploadState as RustBackupUploadState
|
||||||
import org.matrix.rustcomponents.sdk.EnableRecoveryProgress as RustEnableRecoveryProgress
|
import org.matrix.rustcomponents.sdk.EnableRecoveryProgress as RustEnableRecoveryProgress
|
||||||
import org.matrix.rustcomponents.sdk.RecoveryException as RustRecoveryException
|
import org.matrix.rustcomponents.sdk.RecoveryException as RustRecoveryException
|
||||||
|
|
@ -103,14 +105,20 @@ class RustEncryptionService(
|
||||||
* TODO This is a temporary workaround, when we will have a way to observe
|
* TODO This is a temporary workaround, when we will have a way to observe
|
||||||
* the sessions, this code will have to be updated.
|
* the sessions, this code will have to be updated.
|
||||||
*/
|
*/
|
||||||
override val hasDevicesToVerifyAgainst: StateFlow<Boolean> = flow {
|
override val hasDevicesToVerifyAgainst: StateFlow<AsyncData<Boolean>> = flow {
|
||||||
while (currentCoroutineContext().isActive) {
|
while (currentCoroutineContext().isActive) {
|
||||||
val result = hasDevicesToVerifyAgainst().getOrDefault(false)
|
val result = hasDevicesToVerifyAgainst()
|
||||||
emit(result)
|
result
|
||||||
|
.onSuccess {
|
||||||
|
emit(AsyncData.Success(it))
|
||||||
|
}
|
||||||
|
.onFailure {
|
||||||
|
Timber.e(it, "Failed to get hasDevicesToVerifyAgainst, retrying in 5s...")
|
||||||
|
}
|
||||||
delay(5_000)
|
delay(5_000)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.stateIn(sessionCoroutineScope, SharingStarted.Eagerly, false)
|
.stateIn(sessionCoroutineScope, SharingStarted.Eagerly, AsyncData.Uninitialized)
|
||||||
|
|
||||||
override suspend fun enableBackups(): Result<Unit> = withContext(dispatchers.io) {
|
override suspend fun enableBackups(): Result<Unit> = withContext(dispatchers.io) {
|
||||||
runCatchingExceptions {
|
runCatchingExceptions {
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,7 @@ class NotificationMapper(
|
||||||
isDirect = item.roomInfo.isDirect,
|
isDirect = item.roomInfo.isDirect,
|
||||||
activeMembersCount = item.roomInfo.joinedMembersCount.toInt(),
|
activeMembersCount = item.roomInfo.joinedMembersCount.toInt(),
|
||||||
)
|
)
|
||||||
|
val timestamp = item.timestamp() ?: clock.epochMillis()
|
||||||
NotificationData(
|
NotificationData(
|
||||||
sessionId = sessionId,
|
sessionId = sessionId,
|
||||||
eventId = eventId,
|
eventId = eventId,
|
||||||
|
|
@ -53,8 +54,8 @@ class NotificationMapper(
|
||||||
isDm = isDm,
|
isDm = isDm,
|
||||||
isEncrypted = item.roomInfo.isEncrypted.orFalse(),
|
isEncrypted = item.roomInfo.isEncrypted.orFalse(),
|
||||||
isNoisy = item.isNoisy.orFalse(),
|
isNoisy = item.isNoisy.orFalse(),
|
||||||
timestamp = item.timestamp() ?: clock.epochMillis(),
|
timestamp = timestamp,
|
||||||
content = item.event.use { notificationContentMapper.map(it) }.getOrThrow(),
|
content = notificationContentMapper.map(item.event).getOrThrow(),
|
||||||
hasMention = item.hasMention.orFalse(),
|
hasMention = item.hasMention.orFalse(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,8 +25,9 @@ class TimelineEventToNotificationContentMapper {
|
||||||
fun map(timelineEvent: TimelineEvent): Result<NotificationContent> {
|
fun map(timelineEvent: TimelineEvent): Result<NotificationContent> {
|
||||||
return runCatchingExceptions {
|
return runCatchingExceptions {
|
||||||
timelineEvent.use {
|
timelineEvent.use {
|
||||||
|
val senderId = UserId(timelineEvent.senderId())
|
||||||
timelineEvent.eventType().use { eventType ->
|
timelineEvent.eventType().use { eventType ->
|
||||||
eventType.toContent(senderId = UserId(timelineEvent.senderId()))
|
eventType.toContent(senderId = senderId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
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