From 4e085efcf4390d55a7b7d4ec6e82475bc11a2efc Mon Sep 17 00:00:00 2001 From: Jorge Martin Espinosa Date: Mon, 3 Apr 2023 18:02:34 +0200 Subject: [PATCH 1/6] Update Gradle and AGP to v8.0 (#283) * Update dependency gradle to v8 * Handle upgrading Gradle to v8.0.2 * Update AGP to 8.0.0-RC01 * Try to set JAVA_HOME to JDK17 * Update lint version. * Use right JDK for dependency analysis, replace deprecated env var --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build.yml | 5 +++++ .github/workflows/maestro.yml | 5 +++++ .github/workflows/nightly.yml | 5 +++++ .github/workflows/nightly_manual.yml | 5 +++++ .github/workflows/quality.yml | 12 +++++++++++- .github/workflows/tests.yml | 5 +++++ app/build.gradle.kts | 7 +++++-- gradle.properties | 2 +- gradle/libs.versions.toml | 4 +--- gradle/wrapper/gradle-wrapper.properties | 4 ++-- libraries/core/build.gradle.kts | 4 ++-- libraries/coroutines/build.gradle.kts | 4 ++-- libraries/designsystem/build.gradle.kts | 4 ++++ libraries/matrix/api/build.gradle.kts | 4 ++++ libraries/statemachine/build.gradle.kts | 4 ++-- libraries/ui-strings/build.gradle.kts | 7 ------- plugins/src/main/kotlin/Versions.kt | 2 +- plugins/src/main/kotlin/extension/CommonExtension.kt | 4 ++-- .../main/kotlin/extension/DependencyHandleScope.kt | 9 ++++++--- 19 files changed, 68 insertions(+), 28 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 71cfd8bca0..3db9e4c5ef 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -28,6 +28,11 @@ jobs: # Ensure we are building the branch and not the branch after being merged on develop # https://github.com/actions/checkout/issues/881 ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }} + - name: Use JDK 17 + uses: actions/setup-java@v3 + with: + distribution: 'temurin' # See 'Supported distributions' for available options + java-version: '17' - name: Configure gradle uses: gradle/gradle-build-action@v2 with: diff --git a/.github/workflows/maestro.yml b/.github/workflows/maestro.yml index fcd109225d..b359f44b75 100644 --- a/.github/workflows/maestro.yml +++ b/.github/workflows/maestro.yml @@ -24,6 +24,11 @@ jobs: cancel-in-progress: true steps: - uses: actions/checkout@v3 + - uses: actions/setup-java@v3 + name: Use JDK 17 + with: + distribution: 'temurin' # See 'Supported distributions' for available options + java-version: '17' - name: Assemble debug APK run: ./gradlew assembleDebug $CI_GRADLE_ARG_PROPERTIES - uses: mobile-dev-inc/action-maestro-cloud@v1.3.1 diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 2e17f5dbb6..afefa27baa 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -15,6 +15,11 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 + - name: Use JDK 17 + uses: actions/setup-java@v3 + with: + distribution: 'temurin' # See 'Supported distributions' for available options + java-version: '17' - name: Install towncrier run: | python3 -m pip install towncrier diff --git a/.github/workflows/nightly_manual.yml b/.github/workflows/nightly_manual.yml index 707d424817..6e8ef7c684 100644 --- a/.github/workflows/nightly_manual.yml +++ b/.github/workflows/nightly_manual.yml @@ -13,6 +13,11 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 + - name: Use JDK 17 + uses: actions/setup-java@v3 + with: + distribution: 'temurin' # See 'Supported distributions' for available options + java-version: '17' - name: Install towncrier run: | python3 -m pip install towncrier diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index 8f6fe6112c..06d79e857d 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -8,7 +8,7 @@ on: # Enrich gradle.properties for CI/CD env: - GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx3072m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError" -XX:MaxPermSize=512m -Dkotlin.daemon.jvm.options="-Xmx2g" -Dkotlin.incremental=false + GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx3072m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError" -XX:MaxMetaspaceSize=512m -Dkotlin.daemon.jvm.options="-Xmx2g" -Dkotlin.incremental=false CI_GRADLE_ARG_PROPERTIES: --stacktrace -PpreDexEnable=false --max-workers 2 --no-daemon jobs: @@ -21,6 +21,11 @@ jobs: cancel-in-progress: true steps: - uses: actions/checkout@v3 + - name: Use JDK 17 + uses: actions/setup-java@v3 + with: + distribution: 'temurin' # See 'Supported distributions' for available options + java-version: '17' - name: Configure gradle uses: gradle/gradle-build-action@v2 with: @@ -60,6 +65,11 @@ jobs: cancel-in-progress: true steps: - uses: actions/checkout@v3 + - name: Use JDK 17 + uses: actions/setup-java@v3 + with: + distribution: 'temurin' # See 'Supported distributions' for available options + java-version: '17' - name: Configure gradle uses: gradle/gradle-build-action@v2 with: diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index a4fce4b8e6..ada7ba1ab3 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -25,6 +25,11 @@ jobs: uses: actions/checkout@v3 with: lfs: 'true' + - name: ☕️ Use JDK 17 + uses: actions/setup-java@v3 + with: + distribution: 'temurin' # See 'Supported distributions' for available options + java-version: '17' - name: Configure gradle uses: gradle/gradle-build-action@v2 with: diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 50db9aa617..5bf8608224 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -25,7 +25,6 @@ import extension.allServicesImpl @Suppress("DSL_SCOPE_VIOLATION") plugins { id("io.element.android-compose-application") - alias(libs.plugins.stem) alias(libs.plugins.kotlin.android) alias(libs.plugins.anvil) alias(libs.plugins.ksp) @@ -140,7 +139,7 @@ android { } } kotlinOptions { - jvmTarget = "1.8" + jvmTarget = "17" } // Waiting for https://github.com/google/ksp/issues/37 @@ -151,6 +150,10 @@ android { } } } + + buildFeatures { + buildConfig = true + } } androidComponents { diff --git a/gradle.properties b/gradle.properties index df832c13ba..15ee7e1fcb 100644 --- a/gradle.properties +++ b/gradle.properties @@ -46,7 +46,7 @@ signing.element.nightly.keyPassword=Secret # Customise the Lint version to use a more recent version than the one bundled with AGP # https://googlesamples.github.io/android-custom-lint-rules/usage/newer-lint.md.html -android.experimental.lint.version=8.0.0-alpha10 +android.experimental.lint.version=8.0.0-rc01 # Enable test fixture for all modules by default android.experimental.enableTestFixtures=true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index aa7ed975b7..cfec8a2f2d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,7 +3,7 @@ [versions] # Project -android_gradle_plugin = "7.4.2" +android_gradle_plugin = "8.0.0-rc01" firebase_gradle_plugin = "3.2.0" kotlin = "1.8.10" ksp = "1.8.10-1.0.9" @@ -156,8 +156,6 @@ detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" } ktlint = { id = "org.jlleitschuh.gradle.ktlint", version.ref = "ktlint" } dependencygraph = { id = "com.savvasdalkitsis.module-dependency-graph", version.ref = "dependencygraph" } dependencycheck = { id = "org.owasp.dependencycheck", version.ref = "dependencycheck" } -stem = { id = "com.likethesalad.stem", version.ref = "stem" } -stemlibrary = { id = "com.likethesalad.stem-library", version.ref = "stem" } paparazzi = "app.cash.paparazzi:1.2.0" sonarqube = "org.sonarqube:4.0.0.2929" kover = "org.jetbrains.kotlinx.kover:0.6.1" diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 147d0a111f..4b00b4d231 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -16,8 +16,8 @@ #Fri Oct 07 15:02:00 CEST 2022 distributionBase=GRADLE_USER_HOME -distributionSha256Sum=518a863631feb7452b8f1b3dc2aaee5f388355cc3421bbd0275fbeadd77e84b2 -distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-all.zip +distributionSha256Sum=47a5bfed9ef814f90f8debcbbb315e8e7c654109acd224595ea39fca95c5d4da +distributionUrl=https\://services.gradle.org/distributions/gradle-8.0.2-all.zip distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME diff --git a/libraries/core/build.gradle.kts b/libraries/core/build.gradle.kts index dad8f844cc..b3695b861a 100644 --- a/libraries/core/build.gradle.kts +++ b/libraries/core/build.gradle.kts @@ -23,8 +23,8 @@ plugins { } java { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } dependencies { diff --git a/libraries/coroutines/build.gradle.kts b/libraries/coroutines/build.gradle.kts index f9f12e0b09..10cfd0ee80 100644 --- a/libraries/coroutines/build.gradle.kts +++ b/libraries/coroutines/build.gradle.kts @@ -21,8 +21,8 @@ plugins { } java { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } dependencies { diff --git a/libraries/designsystem/build.gradle.kts b/libraries/designsystem/build.gradle.kts index 45430e5d82..aea3c89305 100644 --- a/libraries/designsystem/build.gradle.kts +++ b/libraries/designsystem/build.gradle.kts @@ -24,6 +24,10 @@ plugins { android { namespace = "io.element.android.libraries.designsystem" + buildFeatures { + buildConfig = true + } + dependencies { // Should not be there, but this is a POC implementation(libs.coil.compose) diff --git a/libraries/matrix/api/build.gradle.kts b/libraries/matrix/api/build.gradle.kts index 3a2fd2472a..aee8c7b840 100644 --- a/libraries/matrix/api/build.gradle.kts +++ b/libraries/matrix/api/build.gradle.kts @@ -25,6 +25,10 @@ plugins { android { namespace = "io.element.android.libraries.matrix.api" + + buildFeatures { + buildConfig = true + } } anvil { diff --git a/libraries/statemachine/build.gradle.kts b/libraries/statemachine/build.gradle.kts index 31fe22b5f8..5757c9fe4f 100644 --- a/libraries/statemachine/build.gradle.kts +++ b/libraries/statemachine/build.gradle.kts @@ -23,8 +23,8 @@ plugins { } java { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } dependencies { diff --git a/libraries/ui-strings/build.gradle.kts b/libraries/ui-strings/build.gradle.kts index 33dc7d6ba6..df27a9e5e8 100644 --- a/libraries/ui-strings/build.gradle.kts +++ b/libraries/ui-strings/build.gradle.kts @@ -18,15 +18,8 @@ @Suppress("DSL_SCOPE_VIOLATION") plugins { id("io.element.android-library") - alias(libs.plugins.stemlibrary) } android { namespace = "io.element.android.libraries.ui.strings" } - -// forcing the stem string template generator to be cacheable, without this the templates -// are regenerated causing the app module to recompile its sources -tasks.withType(com.likethesalad.android.templates.common.tasks.BaseTask::class.java) { - outputs.cacheIf { true } -} diff --git a/plugins/src/main/kotlin/Versions.kt b/plugins/src/main/kotlin/Versions.kt index 6236bfd307..36a3a33e70 100644 --- a/plugins/src/main/kotlin/Versions.kt +++ b/plugins/src/main/kotlin/Versions.kt @@ -24,6 +24,6 @@ object Versions { const val compileSdk = 33 const val targetSdk = 33 const val minSdk = 23 - val javaCompileVersion = JavaVersion.VERSION_11 + val javaCompileVersion = JavaVersion.VERSION_17 val javaLanguageVersion: JavaLanguageVersion = JavaLanguageVersion.of(11) } diff --git a/plugins/src/main/kotlin/extension/CommonExtension.kt b/plugins/src/main/kotlin/extension/CommonExtension.kt index 16e9b13c1c..52df186a66 100644 --- a/plugins/src/main/kotlin/extension/CommonExtension.kt +++ b/plugins/src/main/kotlin/extension/CommonExtension.kt @@ -32,8 +32,8 @@ fun CommonExtension<*, *, *, *>.androidConfig(project: Project) { compileOptions { isCoreLibraryDesugaringEnabled = true - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } testOptions { diff --git a/plugins/src/main/kotlin/extension/DependencyHandleScope.kt b/plugins/src/main/kotlin/extension/DependencyHandleScope.kt index 314421ebc8..62ecbd4f96 100644 --- a/plugins/src/main/kotlin/extension/DependencyHandleScope.kt +++ b/plugins/src/main/kotlin/extension/DependencyHandleScope.kt @@ -16,14 +16,17 @@ package extension -import gradle.kotlin.dsl.accessors._71f190358cebd46a469f2989484fd643.androidTestImplementation -import gradle.kotlin.dsl.accessors._71f190358cebd46a469f2989484fd643.debugImplementation -import gradle.kotlin.dsl.accessors._71f190358cebd46a469f2989484fd643.implementation import org.gradle.accessors.dm.LibrariesForLibs import org.gradle.kotlin.dsl.DependencyHandlerScope import org.gradle.kotlin.dsl.project import java.io.File +private fun DependencyHandlerScope.implementation(dependency: Any) = dependencies.add("implementation", dependency) + +private fun DependencyHandlerScope.androidTestImplementation(dependency: Any) = dependencies.add("androidTestImplementation", dependency) + +private fun DependencyHandlerScope.debugImplementation(dependency: Any) = dependencies.add("debugImplementation", dependency) + /** * Dependencies used by all the modules */ From 943d9600c3bf4588429d62b7e83e1f97de7da2c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Tue, 4 Apr 2023 08:24:33 +0200 Subject: [PATCH 2/6] Disable nightly workflow for forks --- .github/workflows/nightly.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index afefa27baa..2f7c10a3d0 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -13,6 +13,7 @@ jobs: nightly: name: Build and publish nightly APK to Firebase runs-on: ubuntu-latest + if: ${{ github.repository == 'vector-im/element-x-android' }} steps: - uses: actions/checkout@v3 - name: Use JDK 17 From c633c20a547bcb09533a6940453830fb46f35797 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 4 Apr 2023 09:16:53 +0200 Subject: [PATCH 3/6] Update dependency com.android.tools:desugar_jdk_libs to v2.0.3 (#281) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- app/build.gradle.kts | 2 +- samples/minimal/build.gradle.kts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 5bf8608224..cc79516e7f 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -213,7 +213,7 @@ dependencies { anvil(projects.anvilcodegen) // https://developer.android.com/studio/write/java8-support#library-desugaring-versions - coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.2") + coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.3") implementation(libs.appyx.core) implementation(libs.androidx.splash) implementation(libs.androidx.corektx) diff --git a/samples/minimal/build.gradle.kts b/samples/minimal/build.gradle.kts index 47f5675a07..9ad0df0c0c 100644 --- a/samples/minimal/build.gradle.kts +++ b/samples/minimal/build.gradle.kts @@ -57,5 +57,5 @@ dependencies { implementation(projects.features.roomlist.impl) implementation(projects.features.login.impl) implementation(libs.coroutines.core) - coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.2") + coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.3") } From 4154a96b540fa5956e2c9b99eb2b09bde8bba501 Mon Sep 17 00:00:00 2001 From: Jorge Martin Espinosa Date: Tue, 4 Apr 2023 16:44:01 +0200 Subject: [PATCH 4/6] Revert "Update Gradle and AGP to v8.0 (#283)" (#295) This reverts commit 4e085efcf4390d55a7b7d4ec6e82475bc11a2efc. --- .github/workflows/build.yml | 5 ----- .github/workflows/maestro.yml | 5 ----- .github/workflows/nightly.yml | 5 ----- .github/workflows/nightly_manual.yml | 5 ----- .github/workflows/quality.yml | 12 +----------- .github/workflows/tests.yml | 5 ----- app/build.gradle.kts | 7 ++----- gradle.properties | 2 +- gradle/libs.versions.toml | 4 +++- gradle/wrapper/gradle-wrapper.properties | 4 ++-- libraries/core/build.gradle.kts | 4 ++-- libraries/coroutines/build.gradle.kts | 4 ++-- libraries/designsystem/build.gradle.kts | 4 ---- libraries/matrix/api/build.gradle.kts | 4 ---- libraries/statemachine/build.gradle.kts | 4 ++-- libraries/ui-strings/build.gradle.kts | 7 +++++++ plugins/src/main/kotlin/Versions.kt | 2 +- plugins/src/main/kotlin/extension/CommonExtension.kt | 4 ++-- .../main/kotlin/extension/DependencyHandleScope.kt | 9 +++------ 19 files changed, 28 insertions(+), 68 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3db9e4c5ef..71cfd8bca0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -28,11 +28,6 @@ jobs: # Ensure we are building the branch and not the branch after being merged on develop # https://github.com/actions/checkout/issues/881 ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }} - - name: Use JDK 17 - uses: actions/setup-java@v3 - with: - distribution: 'temurin' # See 'Supported distributions' for available options - java-version: '17' - name: Configure gradle uses: gradle/gradle-build-action@v2 with: diff --git a/.github/workflows/maestro.yml b/.github/workflows/maestro.yml index b359f44b75..fcd109225d 100644 --- a/.github/workflows/maestro.yml +++ b/.github/workflows/maestro.yml @@ -24,11 +24,6 @@ jobs: cancel-in-progress: true steps: - uses: actions/checkout@v3 - - uses: actions/setup-java@v3 - name: Use JDK 17 - with: - distribution: 'temurin' # See 'Supported distributions' for available options - java-version: '17' - name: Assemble debug APK run: ./gradlew assembleDebug $CI_GRADLE_ARG_PROPERTIES - uses: mobile-dev-inc/action-maestro-cloud@v1.3.1 diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 2f7c10a3d0..e5c8447446 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -16,11 +16,6 @@ jobs: if: ${{ github.repository == 'vector-im/element-x-android' }} steps: - uses: actions/checkout@v3 - - name: Use JDK 17 - uses: actions/setup-java@v3 - with: - distribution: 'temurin' # See 'Supported distributions' for available options - java-version: '17' - name: Install towncrier run: | python3 -m pip install towncrier diff --git a/.github/workflows/nightly_manual.yml b/.github/workflows/nightly_manual.yml index 6e8ef7c684..707d424817 100644 --- a/.github/workflows/nightly_manual.yml +++ b/.github/workflows/nightly_manual.yml @@ -13,11 +13,6 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - - name: Use JDK 17 - uses: actions/setup-java@v3 - with: - distribution: 'temurin' # See 'Supported distributions' for available options - java-version: '17' - name: Install towncrier run: | python3 -m pip install towncrier diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index 06d79e857d..8f6fe6112c 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -8,7 +8,7 @@ on: # Enrich gradle.properties for CI/CD env: - GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx3072m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError" -XX:MaxMetaspaceSize=512m -Dkotlin.daemon.jvm.options="-Xmx2g" -Dkotlin.incremental=false + GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx3072m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError" -XX:MaxPermSize=512m -Dkotlin.daemon.jvm.options="-Xmx2g" -Dkotlin.incremental=false CI_GRADLE_ARG_PROPERTIES: --stacktrace -PpreDexEnable=false --max-workers 2 --no-daemon jobs: @@ -21,11 +21,6 @@ jobs: cancel-in-progress: true steps: - uses: actions/checkout@v3 - - name: Use JDK 17 - uses: actions/setup-java@v3 - with: - distribution: 'temurin' # See 'Supported distributions' for available options - java-version: '17' - name: Configure gradle uses: gradle/gradle-build-action@v2 with: @@ -65,11 +60,6 @@ jobs: cancel-in-progress: true steps: - uses: actions/checkout@v3 - - name: Use JDK 17 - uses: actions/setup-java@v3 - with: - distribution: 'temurin' # See 'Supported distributions' for available options - java-version: '17' - name: Configure gradle uses: gradle/gradle-build-action@v2 with: diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index ada7ba1ab3..a4fce4b8e6 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -25,11 +25,6 @@ jobs: uses: actions/checkout@v3 with: lfs: 'true' - - name: ☕️ Use JDK 17 - uses: actions/setup-java@v3 - with: - distribution: 'temurin' # See 'Supported distributions' for available options - java-version: '17' - name: Configure gradle uses: gradle/gradle-build-action@v2 with: diff --git a/app/build.gradle.kts b/app/build.gradle.kts index cc79516e7f..1e3de32f22 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -25,6 +25,7 @@ import extension.allServicesImpl @Suppress("DSL_SCOPE_VIOLATION") plugins { id("io.element.android-compose-application") + alias(libs.plugins.stem) alias(libs.plugins.kotlin.android) alias(libs.plugins.anvil) alias(libs.plugins.ksp) @@ -139,7 +140,7 @@ android { } } kotlinOptions { - jvmTarget = "17" + jvmTarget = "1.8" } // Waiting for https://github.com/google/ksp/issues/37 @@ -150,10 +151,6 @@ android { } } } - - buildFeatures { - buildConfig = true - } } androidComponents { diff --git a/gradle.properties b/gradle.properties index 15ee7e1fcb..df832c13ba 100644 --- a/gradle.properties +++ b/gradle.properties @@ -46,7 +46,7 @@ signing.element.nightly.keyPassword=Secret # Customise the Lint version to use a more recent version than the one bundled with AGP # https://googlesamples.github.io/android-custom-lint-rules/usage/newer-lint.md.html -android.experimental.lint.version=8.0.0-rc01 +android.experimental.lint.version=8.0.0-alpha10 # Enable test fixture for all modules by default android.experimental.enableTestFixtures=true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index cfec8a2f2d..aa7ed975b7 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,7 +3,7 @@ [versions] # Project -android_gradle_plugin = "8.0.0-rc01" +android_gradle_plugin = "7.4.2" firebase_gradle_plugin = "3.2.0" kotlin = "1.8.10" ksp = "1.8.10-1.0.9" @@ -156,6 +156,8 @@ detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" } ktlint = { id = "org.jlleitschuh.gradle.ktlint", version.ref = "ktlint" } dependencygraph = { id = "com.savvasdalkitsis.module-dependency-graph", version.ref = "dependencygraph" } dependencycheck = { id = "org.owasp.dependencycheck", version.ref = "dependencycheck" } +stem = { id = "com.likethesalad.stem", version.ref = "stem" } +stemlibrary = { id = "com.likethesalad.stem-library", version.ref = "stem" } paparazzi = "app.cash.paparazzi:1.2.0" sonarqube = "org.sonarqube:4.0.0.2929" kover = "org.jetbrains.kotlinx.kover:0.6.1" diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 4b00b4d231..147d0a111f 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -16,8 +16,8 @@ #Fri Oct 07 15:02:00 CEST 2022 distributionBase=GRADLE_USER_HOME -distributionSha256Sum=47a5bfed9ef814f90f8debcbbb315e8e7c654109acd224595ea39fca95c5d4da -distributionUrl=https\://services.gradle.org/distributions/gradle-8.0.2-all.zip +distributionSha256Sum=518a863631feb7452b8f1b3dc2aaee5f388355cc3421bbd0275fbeadd77e84b2 +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-all.zip distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME diff --git a/libraries/core/build.gradle.kts b/libraries/core/build.gradle.kts index b3695b861a..dad8f844cc 100644 --- a/libraries/core/build.gradle.kts +++ b/libraries/core/build.gradle.kts @@ -23,8 +23,8 @@ plugins { } java { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 } dependencies { diff --git a/libraries/coroutines/build.gradle.kts b/libraries/coroutines/build.gradle.kts index 10cfd0ee80..f9f12e0b09 100644 --- a/libraries/coroutines/build.gradle.kts +++ b/libraries/coroutines/build.gradle.kts @@ -21,8 +21,8 @@ plugins { } java { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 } dependencies { diff --git a/libraries/designsystem/build.gradle.kts b/libraries/designsystem/build.gradle.kts index aea3c89305..45430e5d82 100644 --- a/libraries/designsystem/build.gradle.kts +++ b/libraries/designsystem/build.gradle.kts @@ -24,10 +24,6 @@ plugins { android { namespace = "io.element.android.libraries.designsystem" - buildFeatures { - buildConfig = true - } - dependencies { // Should not be there, but this is a POC implementation(libs.coil.compose) diff --git a/libraries/matrix/api/build.gradle.kts b/libraries/matrix/api/build.gradle.kts index aee8c7b840..3a2fd2472a 100644 --- a/libraries/matrix/api/build.gradle.kts +++ b/libraries/matrix/api/build.gradle.kts @@ -25,10 +25,6 @@ plugins { android { namespace = "io.element.android.libraries.matrix.api" - - buildFeatures { - buildConfig = true - } } anvil { diff --git a/libraries/statemachine/build.gradle.kts b/libraries/statemachine/build.gradle.kts index 5757c9fe4f..31fe22b5f8 100644 --- a/libraries/statemachine/build.gradle.kts +++ b/libraries/statemachine/build.gradle.kts @@ -23,8 +23,8 @@ plugins { } java { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 } dependencies { diff --git a/libraries/ui-strings/build.gradle.kts b/libraries/ui-strings/build.gradle.kts index df27a9e5e8..33dc7d6ba6 100644 --- a/libraries/ui-strings/build.gradle.kts +++ b/libraries/ui-strings/build.gradle.kts @@ -18,8 +18,15 @@ @Suppress("DSL_SCOPE_VIOLATION") plugins { id("io.element.android-library") + alias(libs.plugins.stemlibrary) } android { namespace = "io.element.android.libraries.ui.strings" } + +// forcing the stem string template generator to be cacheable, without this the templates +// are regenerated causing the app module to recompile its sources +tasks.withType(com.likethesalad.android.templates.common.tasks.BaseTask::class.java) { + outputs.cacheIf { true } +} diff --git a/plugins/src/main/kotlin/Versions.kt b/plugins/src/main/kotlin/Versions.kt index 36a3a33e70..6236bfd307 100644 --- a/plugins/src/main/kotlin/Versions.kt +++ b/plugins/src/main/kotlin/Versions.kt @@ -24,6 +24,6 @@ object Versions { const val compileSdk = 33 const val targetSdk = 33 const val minSdk = 23 - val javaCompileVersion = JavaVersion.VERSION_17 + val javaCompileVersion = JavaVersion.VERSION_11 val javaLanguageVersion: JavaLanguageVersion = JavaLanguageVersion.of(11) } diff --git a/plugins/src/main/kotlin/extension/CommonExtension.kt b/plugins/src/main/kotlin/extension/CommonExtension.kt index 52df186a66..16e9b13c1c 100644 --- a/plugins/src/main/kotlin/extension/CommonExtension.kt +++ b/plugins/src/main/kotlin/extension/CommonExtension.kt @@ -32,8 +32,8 @@ fun CommonExtension<*, *, *, *>.androidConfig(project: Project) { compileOptions { isCoreLibraryDesugaringEnabled = true - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 } testOptions { diff --git a/plugins/src/main/kotlin/extension/DependencyHandleScope.kt b/plugins/src/main/kotlin/extension/DependencyHandleScope.kt index 62ecbd4f96..314421ebc8 100644 --- a/plugins/src/main/kotlin/extension/DependencyHandleScope.kt +++ b/plugins/src/main/kotlin/extension/DependencyHandleScope.kt @@ -16,17 +16,14 @@ package extension +import gradle.kotlin.dsl.accessors._71f190358cebd46a469f2989484fd643.androidTestImplementation +import gradle.kotlin.dsl.accessors._71f190358cebd46a469f2989484fd643.debugImplementation +import gradle.kotlin.dsl.accessors._71f190358cebd46a469f2989484fd643.implementation import org.gradle.accessors.dm.LibrariesForLibs import org.gradle.kotlin.dsl.DependencyHandlerScope import org.gradle.kotlin.dsl.project import java.io.File -private fun DependencyHandlerScope.implementation(dependency: Any) = dependencies.add("implementation", dependency) - -private fun DependencyHandlerScope.androidTestImplementation(dependency: Any) = dependencies.add("androidTestImplementation", dependency) - -private fun DependencyHandlerScope.debugImplementation(dependency: Any) = dependencies.add("debugImplementation", dependency) - /** * Dependencies used by all the modules */ From f0b95d30beb4571333e196268f066c53a5fe4c58 Mon Sep 17 00:00:00 2001 From: Jorge Martin Espinosa Date: Tue, 4 Apr 2023 16:50:50 +0200 Subject: [PATCH 5/6] Disable Diawi when running from fork (#292) --- .github/workflows/build.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 71cfd8bca0..1607e8efca 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -42,12 +42,14 @@ jobs: app/build/outputs/apk/debug/*.apk - uses: rnkdsh/action-upload-diawi@v1.3.2 id: diawi - if: ${{ github.event_name == 'pull_request' }} - with: + env: token: ${{ secrets.DIAWI_TOKEN }} + if: ${{ github.event_name == 'pull_request' && env.token != '' }} + with: + token: ${{ env.token }} file: app/build/outputs/apk/debug/app-arm64-v8a-debug.apk - name: Add or update PR comment with QR Code to download APK. - if: ${{ github.event_name == 'pull_request' }} + if: ${{ github.event_name == 'pull_request' && steps.diawi.conclusion == 'success' }} uses: NejcZdovc/comment-pr@v2 with: message: | From d7a6779343a3cd513584b63b217e3affe82f3955 Mon Sep 17 00:00:00 2001 From: Jorge Martin Espinosa Date: Tue, 4 Apr 2023 18:07:57 +0200 Subject: [PATCH 6/6] [Room member list] Display room member list (#276) * Implement room member list * Move timeline initialization back to `TimelinePresenter`. * Fix crash when the `innerRoom` inside a `RustMatrixRoom` is destroyed but `syncUpdateFlow` is still running. * Address review comments --- .../io/element/android/appnav/RoomFlowNode.kt | 3 + .../android/appnav/RoomFlowPresenter.kt | 46 ++++++ .../android/appnav/RoomFlowPresenterTest.kt | 62 ++++++++ changelog.d/251.feature | 1 + features/createroom/impl/build.gradle.kts | 5 +- .../impl/AllMatrixUsersDataSource.kt | 33 ++++ .../impl/addpeople/AddPeoplePresenter.kt | 23 ++- .../impl/addpeople/AddPeopleState.kt | 4 +- .../impl/addpeople/AddPeopleStateProvider.kt | 12 +- .../impl/addpeople/AddPeopleView.kt | 10 +- .../createroom/impl/di/CreateRoomModule.kt | 35 +++++ .../impl/root/CreateRoomRootPresenter.kt | 20 ++- .../impl/root/CreateRoomRootState.kt | 4 +- .../impl/root/CreateRoomRootStateProvider.kt | 4 +- .../impl/root/CreateRoomRootView.kt | 10 +- .../impl/addpeople/AddPeoplePresenterTests.kt | 14 +- .../impl/root/CreateRoomRootPresenterTests.kt | 14 +- features/roomdetails/impl/build.gradle.kts | 3 + .../roomdetails/impl/RoomDetailsFlowNode.kt | 18 ++- .../roomdetails/impl/RoomDetailsNode.kt | 8 + .../roomdetails/impl/RoomDetailsPresenter.kt | 21 ++- .../roomdetails/impl/RoomDetailsState.kt | 4 +- .../impl/RoomDetailsStateProvider.kt | 4 +- .../roomdetails/impl/RoomDetailsView.kt | 22 ++- .../roomdetails/impl/di/RoomMemberModule.kt | 35 +++++ .../impl/members/RoomMatrixUserDataSource.kt | 58 +++++++ .../impl/members/RoomMemberListNode.kt | 52 +++++++ .../impl/members/RoomMemberListPresenter.kt | 64 ++++++++ .../impl/members/RoomMemberListState.kt | 28 ++++ .../members/RoomMemberListStateProvider.kt | 42 +++++ .../impl/members/RoomMemberListView.kt | 145 ++++++++++++++++++ .../impl/src/main/res/values/localazy.xml | 4 + .../roomdetails/RoomDetailsPresenterTests.kt | 38 ++++- .../members/RoomMemberListPresenterTests.kt | 62 ++++++++ .../api/build.gradle.kts | 2 +- .../userlist/api/MatrixUserDataSource.kt | 25 +++ .../features/userlist/api/UserListEvents.kt} | 12 +- .../userlist/api/UserListPresenter.kt} | 6 +- .../userlist/api/UserListPresenterArgs.kt} | 4 +- .../features/userlist/api/UserListState.kt} | 6 +- .../userlist/api/UserListStateProvider.kt} | 22 +-- .../features/userlist/api/UserListView.kt} | 24 +-- .../impl/build.gradle.kts | 5 +- .../impl/DefaultUserListPresenter.kt} | 42 ++--- .../impl/DefaultUserListPresenterTests.kt} | 55 ++++--- features/userlist/test/build.gradle.kts | 32 ++++ .../userlist/test/FakeMatrixUserDataSource.kt | 39 +++++ .../android/libraries/architecture/Async.kt | 4 + .../components/preferences/PreferenceText.kt | 16 +- .../libraries/matrix/api/room/MatrixRoom.kt | 4 +- .../matrix/impl/room/RustMatrixRoom.kt | 30 +++- .../impl/timeline/RustMatrixTimeline.kt | 8 - .../matrix/test/room/FakeMatrixRoom.kt | 29 +++- .../src/main/res/values-ro/translations.xml | 10 ++ .../src/main/res/values/localazy.xml | 11 +- ...stDarkPreview_0_null_0,NEXUS_5,1.0,en].png | 3 + ...stDarkPreview_0_null_1,NEXUS_5,1.0,en].png | 3 + ...tLightPreview_0_null_0,NEXUS_5,1.0,en].png | 3 + ...tLightPreview_0_null_1,NEXUS_5,1.0,en].png | 3 + ...lsDarkPreview_0_null_4,NEXUS_5,1.0,en].png | 3 + ...sLightPreview_0_null_4,NEXUS_5,1.0,en].png | 3 + tools/localazy/config.json | 4 +- 62 files changed, 1159 insertions(+), 157 deletions(-) create mode 100644 appnav/src/main/kotlin/io/element/android/appnav/RoomFlowPresenter.kt create mode 100644 appnav/src/test/kotlin/io/element/android/appnav/RoomFlowPresenterTest.kt create mode 100644 features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/AllMatrixUsersDataSource.kt create mode 100644 features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/di/CreateRoomModule.kt create mode 100644 features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/di/RoomMemberModule.kt create mode 100644 features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMatrixUserDataSource.kt create mode 100644 features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListNode.kt create mode 100644 features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenter.kt create mode 100644 features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListState.kt create mode 100644 features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListStateProvider.kt create mode 100644 features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListView.kt create mode 100644 features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/RoomMemberListPresenterTests.kt rename features/{selectusers => userlist}/api/build.gradle.kts (93%) create mode 100644 features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/MatrixUserDataSource.kt rename features/{selectusers/api/src/main/kotlin/io/element/android/features/selectusers/api/SelectUsersEvents.kt => userlist/api/src/main/kotlin/io/element/android/features/userlist/api/UserListEvents.kt} (68%) rename features/{selectusers/api/src/main/kotlin/io/element/android/features/selectusers/api/SelectUsersPresenter.kt => userlist/api/src/main/kotlin/io/element/android/features/userlist/api/UserListPresenter.kt} (76%) rename features/{selectusers/api/src/main/kotlin/io/element/android/features/selectusers/api/SelectUsersPresenterArgs.kt => userlist/api/src/main/kotlin/io/element/android/features/userlist/api/UserListPresenterArgs.kt} (88%) rename features/{selectusers/api/src/main/kotlin/io/element/android/features/selectusers/api/SelectUsersState.kt => userlist/api/src/main/kotlin/io/element/android/features/userlist/api/UserListState.kt} (89%) rename features/{selectusers/api/src/main/kotlin/io/element/android/features/selectusers/api/SelectUsersStateProvider.kt => userlist/api/src/main/kotlin/io/element/android/features/userlist/api/UserListStateProvider.kt} (82%) rename features/{selectusers/api/src/main/kotlin/io/element/android/features/selectusers/api/SelectUsersView.kt => userlist/api/src/main/kotlin/io/element/android/features/userlist/api/UserListView.kt} (92%) rename features/{selectusers => userlist}/impl/build.gradle.kts (92%) rename features/{selectusers/impl/src/main/kotlin/io/element/android/features/selectusers/impl/DefaultSelectUsersPresenter.kt => userlist/impl/src/main/kotlin/io/element/android/features/userlist/impl/DefaultUserListPresenter.kt} (73%) rename features/{selectusers/impl/src/test/kotlin/io/element/android/features/selectusers/impl/DefaultSelectUsersPresenterTests.kt => userlist/impl/src/test/kotlin/io/element/android/features/userlist/impl/DefaultUserListPresenterTests.kt} (68%) create mode 100644 features/userlist/test/build.gradle.kts create mode 100644 features/userlist/test/src/main/kotlin/io/element/android/features/userlist/test/FakeMatrixUserDataSource.kt create mode 100644 libraries/ui-strings/src/main/res/values-ro/translations.xml create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members_null_DefaultGroup_RoomMemberListDarkPreview_0_null_0,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members_null_DefaultGroup_RoomMemberListDarkPreview_0_null_1,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members_null_DefaultGroup_RoomMemberListLightPreview_0_null_0,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members_null_DefaultGroup_RoomMemberListLightPreview_0_null_1,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview_0_null_4,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview_0_null_4,NEXUS_5,1.0,en].png diff --git a/appnav/src/main/kotlin/io/element/android/appnav/RoomFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/RoomFlowNode.kt index 6ef47e4a61..14b90b0064 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/RoomFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/RoomFlowNode.kt @@ -69,6 +69,8 @@ class RoomFlowNode @AssistedInject constructor( private val inputs: Inputs = inputs() + private val roomFlowPresenter = RoomFlowPresenter(inputs.room) + init { lifecycle.subscribe( onCreate = { @@ -110,6 +112,7 @@ class RoomFlowNode @AssistedInject constructor( @Composable override fun View(modifier: Modifier) { + roomFlowPresenter.present() Children( navModel = backstack, modifier = modifier, diff --git a/appnav/src/main/kotlin/io/element/android/appnav/RoomFlowPresenter.kt b/appnav/src/main/kotlin/io/element/android/appnav/RoomFlowPresenter.kt new file mode 100644 index 0000000000..0a2b066da4 --- /dev/null +++ b/appnav/src/main/kotlin/io/element/android/appnav/RoomFlowPresenter.kt @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.appnav + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.matrix.api.room.MatrixRoom +import timber.log.Timber + +class RoomFlowPresenter( + private val room: MatrixRoom, +) : Presenter { + + @Composable + override fun present(): RoomFlowState { + // Preload room members so we can quickly detect if the room is a DM room + LaunchedEffect(Unit) { + room.fetchMembers() + .onFailure { + Timber.e(it, "Fail to fetch members for room ${room.roomId}") + }.onSuccess { + Timber.v("Success fetching members for room ${room.roomId}") + } + } + + return RoomFlowState + } +} + +// At first the return type was Unit, but detekt complained about it +object RoomFlowState diff --git a/appnav/src/test/kotlin/io/element/android/appnav/RoomFlowPresenterTest.kt b/appnav/src/test/kotlin/io/element/android/appnav/RoomFlowPresenterTest.kt new file mode 100644 index 0000000000..1347b0b24c --- /dev/null +++ b/appnav/src/test/kotlin/io/element/android/appnav/RoomFlowPresenterTest.kt @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.appnav + +import app.cash.molecule.RecompositionClock +import app.cash.molecule.moleculeFlow +import app.cash.turbine.test +import com.google.common.truth.Truth +import io.element.android.libraries.matrix.test.room.FakeMatrixRoom +import io.element.android.libraries.matrix.test.timeline.FakeMatrixTimeline +import kotlinx.coroutines.test.runTest +import org.junit.Test +import java.lang.IllegalStateException + +class RoomFlowPresenterTest { + + @Test + fun `present - fetches room members`() = runTest { + val fakeTimeline = FakeMatrixTimeline() + val room = FakeMatrixRoom(matrixTimeline = fakeTimeline) + val presenter = RoomFlowPresenter(room) + + Truth.assertThat(room.areMembersFetched).isFalse() + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + Truth.assertThat(room.areMembersFetched).isTrue() + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - recovers from error while fetching room members`() = runTest { + val fakeTimeline = FakeMatrixTimeline() + val room = FakeMatrixRoom(matrixTimeline = fakeTimeline).apply { + givenFetchMemberResult(Result.failure(IllegalStateException("Some error"))) + } + val presenter = RoomFlowPresenter(room) + + Truth.assertThat(room.areMembersFetched).isFalse() + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + Truth.assertThat(room.areMembersFetched).isFalse() + cancelAndIgnoreRemainingEvents() + } + } +} diff --git a/changelog.d/251.feature b/changelog.d/251.feature index 8c7bb95fd7..209e6e6f71 100644 --- a/changelog.d/251.feature +++ b/changelog.d/251.feature @@ -1 +1,2 @@ Implement Room Details screen +Implement Room Member List screen diff --git a/features/createroom/impl/build.gradle.kts b/features/createroom/impl/build.gradle.kts index 366cc1e0bd..6f2544822c 100644 --- a/features/createroom/impl/build.gradle.kts +++ b/features/createroom/impl/build.gradle.kts @@ -47,7 +47,7 @@ dependencies { implementation(projects.libraries.designsystem) implementation(projects.libraries.elementresources) implementation(projects.libraries.uiStrings) - implementation(projects.features.selectusers.api) + implementation(projects.features.userlist.api) api(projects.features.createroom.api) testImplementation(libs.test.junit) @@ -56,7 +56,8 @@ dependencies { testImplementation(libs.test.truth) testImplementation(libs.test.turbine) testImplementation(projects.libraries.matrix.test) - testImplementation(projects.features.selectusers.impl) + testImplementation(projects.features.userlist.impl) + testImplementation(projects.features.userlist.test) androidTestImplementation(libs.test.junitext) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/AllMatrixUsersDataSource.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/AllMatrixUsersDataSource.kt new file mode 100644 index 0000000000..6bbdfb5e93 --- /dev/null +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/AllMatrixUsersDataSource.kt @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.createroom.impl + +import io.element.android.features.userlist.api.MatrixUserDataSource +import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.ui.model.MatrixUser +import javax.inject.Inject + +// TODO this is empty as we currently don't have an endpoint to perform user search +class AllMatrixUsersDataSource @Inject constructor() : MatrixUserDataSource { + override suspend fun search(query: String): List { + return emptyList() + } + + override suspend fun getProfile(userId: UserId): MatrixUser? { + return null + } +} diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeoplePresenter.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeoplePresenter.kt index 51b2928862..da51a36335 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeoplePresenter.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeoplePresenter.kt @@ -17,31 +17,38 @@ package io.element.android.features.createroom.impl.addpeople import androidx.compose.runtime.Composable -import io.element.android.features.selectusers.api.SelectUsersPresenter -import io.element.android.features.selectusers.api.SelectUsersPresenterArgs -import io.element.android.features.selectusers.api.SelectionMode +import io.element.android.features.userlist.api.SelectionMode +import io.element.android.features.userlist.api.MatrixUserDataSource +import io.element.android.features.userlist.api.UserListPresenter +import io.element.android.features.userlist.api.UserListPresenterArgs import io.element.android.libraries.architecture.Presenter import javax.inject.Inject +import javax.inject.Named class AddPeoplePresenter @Inject constructor( - private val selectUsersPresenterFactory: SelectUsersPresenter.Factory, + private val userListPresenterFactory: UserListPresenter.Factory, + @Named("AllUsers") private val matrixUserDataSource: MatrixUserDataSource, ) : Presenter { - private val selectUsersPresenter by lazy { - selectUsersPresenterFactory.create(SelectUsersPresenterArgs(SelectionMode.Multiple)) + private val userListPresenter by lazy { + userListPresenterFactory.create( + UserListPresenterArgs(selectionMode = SelectionMode.Multiple), + matrixUserDataSource, + ) } @Composable override fun present(): AddPeopleState { - val selectUsersState = selectUsersPresenter.present() + val userListState = userListPresenter.present() fun handleEvents(event: AddPeopleEvents) { // do nothing for now } return AddPeopleState( - selectUsersState = selectUsersState, + userListState = userListState, eventSink = ::handleEvents, ) } } + diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleState.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleState.kt index 8212d02cc4..8605e1aba6 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleState.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleState.kt @@ -16,9 +16,9 @@ package io.element.android.features.createroom.impl.addpeople -import io.element.android.features.selectusers.api.SelectUsersState +import io.element.android.features.userlist.api.UserListState data class AddPeopleState( - val selectUsersState: SelectUsersState, + val userListState: UserListState, val eventSink: (AddPeopleEvents) -> Unit, ) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleStateProvider.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleStateProvider.kt index 6f1679e252..cfbf7941ce 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleStateProvider.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleStateProvider.kt @@ -17,22 +17,22 @@ package io.element.android.features.createroom.impl.addpeople import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import io.element.android.features.selectusers.api.SelectionMode -import io.element.android.features.selectusers.api.aListOfSelectedUsers -import io.element.android.features.selectusers.api.aSelectUsersState +import io.element.android.features.userlist.api.SelectionMode +import io.element.android.features.userlist.api.aListOfSelectedUsers +import io.element.android.features.userlist.api.aUserListState open class AddPeopleStateProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aAddPeopleState(), aAddPeopleState().copy( - selectUsersState = aSelectUsersState().copy( + userListState = aUserListState().copy( selectedUsers = aListOfSelectedUsers(), selectionMode = SelectionMode.Multiple, ) ), aAddPeopleState().copy( - selectUsersState = aSelectUsersState().copy( + userListState = aUserListState().copy( selectedUsers = aListOfSelectedUsers(), isSearchActive = true, selectionMode = SelectionMode.Multiple, @@ -42,6 +42,6 @@ open class AddPeopleStateProvider : PreviewParameterProvider { } fun aAddPeopleState() = AddPeopleState( - selectUsersState = aSelectUsersState(), + userListState = aUserListState(), eventSink = {} ) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleView.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleView.kt index 56b3c975c5..56a16b24f9 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleView.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleView.kt @@ -29,8 +29,8 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import io.element.android.features.userlist.api.UserListView import io.element.android.features.createroom.impl.R -import io.element.android.features.selectusers.api.SelectUsersView import io.element.android.libraries.designsystem.components.button.BackButton import io.element.android.libraries.designsystem.preview.ElementPreviewDark import io.element.android.libraries.designsystem.preview.ElementPreviewLight @@ -52,9 +52,9 @@ fun AddPeopleView( Scaffold( topBar = { - if (!state.selectUsersState.isSearchActive) { + if (!state.userListState.isSearchActive) { AddPeopleViewTopBar( - hasSelectedUsers = state.selectUsersState.selectedUsers.isNotEmpty(), + hasSelectedUsers = state.userListState.selectedUsers.isNotEmpty(), onBackPressed = onBackPressed, onNextPressed = onNextPressed, ) @@ -66,9 +66,9 @@ fun AddPeopleView( .fillMaxSize() .padding(padding), ) { - SelectUsersView( + UserListView( modifier = Modifier.fillMaxWidth(), - state = state.selectUsersState, + state = state.userListState, ) } } diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/di/CreateRoomModule.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/di/CreateRoomModule.kt new file mode 100644 index 0000000000..c5f2d0ca06 --- /dev/null +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/di/CreateRoomModule.kt @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.createroom.impl.di + +import com.squareup.anvil.annotations.ContributesTo +import dagger.Binds +import dagger.Module +import io.element.android.features.createroom.impl.AllMatrixUsersDataSource +import io.element.android.features.userlist.api.MatrixUserDataSource +import io.element.android.libraries.di.AppScope +import javax.inject.Named + +@Module +@ContributesTo(AppScope::class) +interface CreateRoomModule { + + @Binds + @Named("AllUsers") + fun bindAllUserListDataSource(dataSource: AllMatrixUsersDataSource): MatrixUserDataSource + +} diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenter.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenter.kt index 2f3f3ded4a..1250031710 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenter.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenter.kt @@ -17,25 +17,31 @@ package io.element.android.features.createroom.impl.root import androidx.compose.runtime.Composable -import io.element.android.features.selectusers.api.SelectUsersPresenter -import io.element.android.features.selectusers.api.SelectUsersPresenterArgs -import io.element.android.features.selectusers.api.SelectionMode +import io.element.android.features.userlist.api.SelectionMode +import io.element.android.features.userlist.api.MatrixUserDataSource +import io.element.android.features.userlist.api.UserListPresenter +import io.element.android.features.userlist.api.UserListPresenterArgs import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.matrix.ui.model.MatrixUser import timber.log.Timber import javax.inject.Inject +import javax.inject.Named class CreateRoomRootPresenter @Inject constructor( - private val presenterFactory: SelectUsersPresenter.Factory, + private val presenterFactory: UserListPresenter.Factory, + @Named("AllUsers") private val matrixUserDataSource: MatrixUserDataSource, ) : Presenter { private val presenter by lazy { - presenterFactory.create(SelectUsersPresenterArgs(SelectionMode.Single)) + presenterFactory.create( + UserListPresenterArgs(selectionMode = SelectionMode.Single), + matrixUserDataSource, + ) } @Composable override fun present(): CreateRoomRootState { - val selectUsersState = presenter.present() + val userListState = presenter.present() fun handleEvents(event: CreateRoomRootEvents) { when (event) { @@ -45,7 +51,7 @@ class CreateRoomRootPresenter @Inject constructor( } return CreateRoomRootState( - selectUsersState = selectUsersState, + userListState = userListState, eventSink = ::handleEvents, ) } diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootState.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootState.kt index a57d6aaaf6..d5d75fcfae 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootState.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootState.kt @@ -16,9 +16,9 @@ package io.element.android.features.createroom.impl.root -import io.element.android.features.selectusers.api.SelectUsersState +import io.element.android.features.userlist.api.UserListState data class CreateRoomRootState( - val selectUsersState: SelectUsersState, + val userListState: UserListState, val eventSink: (CreateRoomRootEvents) -> Unit, ) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootStateProvider.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootStateProvider.kt index 678f02476c..d7c18085dc 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootStateProvider.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootStateProvider.kt @@ -17,7 +17,7 @@ package io.element.android.features.createroom.impl.root import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import io.element.android.features.selectusers.api.aSelectUsersState +import io.element.android.features.userlist.api.aUserListState open class CreateRoomRootStateProvider : PreviewParameterProvider { override val values: Sequence @@ -28,5 +28,5 @@ open class CreateRoomRootStateProvider : PreviewParameterProvider createNode(buildContext) + NavTarget.RoomDetails -> createNode(buildContext, listOf(callback)) + NavTarget.RoomMemberList -> createNode(buildContext) } } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsNode.kt index 8ba373756b..937d755df5 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsNode.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsNode.kt @@ -23,6 +23,7 @@ import androidx.compose.ui.platform.LocalContext import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.plugin.Plugin +import com.bumble.appyx.core.plugin.plugins import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode @@ -40,6 +41,12 @@ class RoomDetailsNode @AssistedInject constructor( private val room: MatrixRoom, ) : Node(buildContext, plugins = plugins) { + private val callback = plugins().firstOrNull() + + private fun openRoomMemberList() { + callback?.openRoomMemberList() + } + private fun onShareRoom(context: Context) { val alias = room.alias ?: room.alternativeAliases.firstOrNull() val permalinkResult = alias?.let { PermalinkBuilder.permalinkForRoomAlias(it) } @@ -64,6 +71,7 @@ class RoomDetailsNode @AssistedInject constructor( modifier = modifier, goBack = { navigateUp() }, onShareRoom = { onShareRoom(context) }, + openRoomMemberList = ::openRoomMemberList, ) } } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt index f038787909..80ae4426d9 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt @@ -17,8 +17,16 @@ package io.element.android.features.roomdetails.impl import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.matrix.api.room.MatrixRoom +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import javax.inject.Inject class RoomDetailsPresenter @Inject constructor( @@ -29,13 +37,24 @@ class RoomDetailsPresenter @Inject constructor( override fun present(): RoomDetailsState { // fun handleEvents(event: RoomDetailsEvent) {} + var memberCount: Async by remember { mutableStateOf(Async.Loading()) } + LaunchedEffect(Unit) { + withContext(Dispatchers.IO) { + memberCount = runCatching { room.memberCount() } + .fold( + onSuccess = { Async.Success(it) }, + onFailure = { Async.Failure(it) } + ) + } + } + return RoomDetailsState( roomId = room.roomId.value, roomName = room.name ?: room.displayName, roomAlias = room.alias, roomAvatarUrl = room.avatarUrl, roomTopic = room.topic, - memberCount = room.members.size, + memberCount = memberCount, isEncrypted = room.isEncrypted, // eventSink = ::handleEvents ) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsState.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsState.kt index ccad10bbc4..78ee70529d 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsState.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsState.kt @@ -16,13 +16,15 @@ package io.element.android.features.roomdetails.impl +import io.element.android.libraries.architecture.Async + data class RoomDetailsState( val roomId: String, val roomName: String, val roomAlias: String?, val roomAvatarUrl: String?, val roomTopic: String?, - val memberCount: Int, + val memberCount: Async, val isEncrypted: Boolean, // val eventSink: (RoomDetailsEvent) -> Unit ) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsStateProvider.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsStateProvider.kt index 29ecc4f995..c91db4d3a8 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsStateProvider.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsStateProvider.kt @@ -17,6 +17,7 @@ package io.element.android.features.roomdetails.impl import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.libraries.architecture.Async open class RoomDetailsStateProvider : PreviewParameterProvider { override val values: Sequence @@ -25,6 +26,7 @@ open class RoomDetailsStateProvider : PreviewParameterProvider aRoomDetailsState().copy(roomTopic = null), aRoomDetailsState().copy(isEncrypted = false), aRoomDetailsState().copy(roomAlias = null), + aRoomDetailsState().copy(memberCount = Async.Failure(Throwable())), // Add other state here ) } @@ -39,7 +41,7 @@ fun aRoomDetailsState() = RoomDetailsState( "|| MAIL iki/Marketing " + "|| MAI iki/Marketing " + "|| MAI iki/Marketing...", - memberCount = 32, + memberCount = Async.Success(32), isEncrypted = true, // eventSink = {} ) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt index 322bc5f8ac..e4d4ac5609 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt @@ -42,6 +42,8 @@ import androidx.compose.ui.res.vectorResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp +import io.element.android.libraries.architecture.Async +import io.element.android.libraries.architecture.isLoading import io.element.android.libraries.designsystem.ElementTextStyles import io.element.android.libraries.designsystem.components.avatar.Avatar import io.element.android.libraries.designsystem.components.avatar.AvatarData @@ -62,6 +64,7 @@ fun RoomDetailsView( state: RoomDetailsState, goBack: () -> Unit, onShareRoom: () -> Unit, + openRoomMemberList: () -> Unit, modifier: Modifier = Modifier, ) { Scaffold( @@ -87,7 +90,12 @@ fun RoomDetailsView( TopicSection(roomTopic = state.roomTopic) } - MembersSection(memberCount = state.memberCount) + val memberCount = (state.memberCount as? Async.Success)?.state + MembersSection( + memberCount = memberCount, + isLoading = state.memberCount.isLoading(), + openRoomMemberList = openRoomMemberList + ) if (state.isEncrypted) { SecuritySection() @@ -148,12 +156,19 @@ internal fun TopicSection(roomTopic: String, modifier: Modifier = Modifier) { } @Composable -internal fun MembersSection(memberCount: Int, modifier: Modifier = Modifier) { +internal fun MembersSection( + memberCount: Int?, + isLoading: Boolean, + modifier: Modifier = Modifier, + openRoomMemberList: () -> Unit +) { PreferenceCategory(modifier = modifier) { PreferenceText( title = stringResource(R.string.screen_room_details_people_title), icon = Icons.Outlined.Person, - currentValue = memberCount.toString(), + currentValue = memberCount?.toString(), + onClick = openRoomMemberList, + loadingCurrentValue = isLoading, ) PreferenceText( title = stringResource(R.string.screen_room_details_invite_people_title), @@ -200,5 +215,6 @@ private fun ContentToPreview(state: RoomDetailsState) { state = state, goBack = {}, onShareRoom = {}, + openRoomMemberList = {}, ) } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/di/RoomMemberModule.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/di/RoomMemberModule.kt new file mode 100644 index 0000000000..49c98374f2 --- /dev/null +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/di/RoomMemberModule.kt @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.roomdetails.impl.di + +import com.squareup.anvil.annotations.ContributesTo +import dagger.Binds +import dagger.Module +import io.element.android.features.roomdetails.impl.members.RoomMatrixUserDataSource +import io.element.android.features.userlist.api.MatrixUserDataSource +import io.element.android.libraries.di.RoomScope +import javax.inject.Named + +@Module +@ContributesTo(RoomScope::class) +interface RoomMemberModule { + + @Binds + @Named("RoomMembers") + fun bindRoomMemberUserListDataSource(dataSource: RoomMatrixUserDataSource): MatrixUserDataSource + +} diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMatrixUserDataSource.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMatrixUserDataSource.kt new file mode 100644 index 0000000000..b97dcd62e5 --- /dev/null +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMatrixUserDataSource.kt @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.roomdetails.impl.members + +import io.element.android.features.userlist.api.MatrixUserDataSource +import io.element.android.libraries.core.bool.orFalse +import io.element.android.libraries.designsystem.components.avatar.AvatarData +import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.room.MatrixRoom +import io.element.android.libraries.matrix.api.room.RoomMember +import io.element.android.libraries.matrix.ui.model.MatrixUser +import javax.inject.Inject + +class RoomMatrixUserDataSource @Inject constructor( + private val room: MatrixRoom +) : MatrixUserDataSource { + + override suspend fun search(query: String): List { + return room.members().filter { member -> + if (query.isBlank()) { + true + } else { + member.userId.contains(query, ignoreCase = true) || member.displayName?.contains(query, ignoreCase = true).orFalse() + } + }.map(::mapMemberToMatrixUser) + } + + override suspend fun getProfile(userId: UserId): MatrixUser? { + return null + } + + private fun mapMemberToMatrixUser(member: RoomMember): MatrixUser { + return MatrixUser( + id = UserId(member.userId), + username = member.displayName, + avatarData = AvatarData( + id = member.userId, + name = member.displayName, + url = member.avatarUrl + ) + ) + } + +} diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListNode.kt new file mode 100644 index 0000000000..0f65b4657b --- /dev/null +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListNode.kt @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.roomdetails.impl.members + +import androidx.compose.runtime.Composable +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 dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import io.element.android.anvilannotations.ContributesNode +import io.element.android.libraries.di.RoomScope +import io.element.android.libraries.matrix.ui.model.MatrixUser +import timber.log.Timber + +@ContributesNode(RoomScope::class) +class RoomMemberListNode @AssistedInject constructor( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, + private val presenter: RoomMemberListPresenter, +) : Node(buildContext, plugins = plugins) { + + private fun onUserSelected(matrixUser: MatrixUser) { + Timber.d("TODO: implement user selection. User: $matrixUser") + } + + @Composable + override fun View(modifier: Modifier) { + val state = presenter.present() + RoomMemberListView( + state = state, + modifier = modifier, + onBackPressed = { navigateUp() }, + onUserSelected = ::onUserSelected, + ) + } +} diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenter.kt new file mode 100644 index 0000000000..8c3a873ade --- /dev/null +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenter.kt @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.roomdetails.impl.members + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import io.element.android.features.userlist.api.SelectionMode +import io.element.android.features.userlist.api.MatrixUserDataSource +import io.element.android.features.userlist.api.UserListPresenter +import io.element.android.features.userlist.api.UserListPresenterArgs +import io.element.android.libraries.architecture.Async +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.matrix.ui.model.MatrixUser +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import javax.inject.Inject +import javax.inject.Named + +class RoomMemberListPresenter @Inject constructor( + private val userListPresenterFactory: UserListPresenter.Factory, + @Named("RoomMembers") private val matrixUserDataSource: MatrixUserDataSource, +) : Presenter { + + private val userListPresenter by lazy { + userListPresenterFactory.create( + UserListPresenterArgs(selectionMode = SelectionMode.Single), + matrixUserDataSource, + ) + } + + @Composable + override fun present(): RoomMemberListState { + val userListState = userListPresenter.present() + val allUsers = remember { mutableStateOf>>(Async.Loading()) } + LaunchedEffect(Unit) { + withContext(Dispatchers.IO) { + allUsers.value = Async.Success(matrixUserDataSource.search("").toImmutableList()) + } + } + return RoomMemberListState( + allUsers = allUsers.value, + userListState = userListState + ) + } +} + diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListState.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListState.kt new file mode 100644 index 0000000000..f5e5bd3efb --- /dev/null +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListState.kt @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.roomdetails.impl.members + +import io.element.android.features.userlist.api.UserListState +import io.element.android.libraries.architecture.Async +import io.element.android.libraries.matrix.ui.model.MatrixUser +import kotlinx.collections.immutable.ImmutableList + +data class RoomMemberListState( + val allUsers: Async>, + val userListState: UserListState, +// val eventSink: (AddPeopleEvents) -> Unit, +) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListStateProvider.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListStateProvider.kt new file mode 100644 index 0000000000..fc98ae7544 --- /dev/null +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListStateProvider.kt @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.roomdetails.impl.members + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.features.userlist.api.aUserListState +import io.element.android.libraries.architecture.Async +import io.element.android.libraries.matrix.ui.components.aMatrixUser +import io.element.android.libraries.matrix.ui.model.MatrixUser +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf + +internal class RoomMemberListStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + aRoomMemberListState(allUsers = Async.Success(persistentListOf(aMatrixUser()))), + aRoomMemberListState(allUsers = Async.Loading()) + ) +} + +internal fun aRoomMemberListState( + searchResults: ImmutableList = persistentListOf(), + allUsers: Async> = Async.Uninitialized, +) = + RoomMemberListState( + userListState = aUserListState().copy(searchResults = searchResults), + allUsers = allUsers, + ) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListView.kt new file mode 100644 index 0000000000..e2c41e34b3 --- /dev/null +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListView.kt @@ -0,0 +1,145 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.roomdetails.impl.members + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.pluralStringResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import io.element.android.features.roomdetails.impl.R +import io.element.android.features.userlist.api.SearchSingleUserResultItem +import io.element.android.features.userlist.api.UserListView +import io.element.android.libraries.architecture.Async +import io.element.android.libraries.architecture.isLoading +import io.element.android.libraries.designsystem.ElementTextStyles +import io.element.android.libraries.designsystem.components.button.BackButton +import io.element.android.libraries.designsystem.preview.ElementPreviewDark +import io.element.android.libraries.designsystem.preview.ElementPreviewLight +import io.element.android.libraries.designsystem.theme.components.CenterAlignedTopAppBar +import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator +import io.element.android.libraries.designsystem.theme.components.Scaffold +import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.matrix.ui.model.MatrixUser + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun RoomMemberListView( + state: RoomMemberListState, + modifier: Modifier = Modifier, + onBackPressed: () -> Unit = {}, + onUserSelected: (MatrixUser) -> Unit = {}, +) { + Scaffold( + topBar = { + if (!state.userListState.isSearchActive) { + RoomMemberListTopBar(onBackPressed = onBackPressed) + } + } + ) { padding -> + Column( + modifier = modifier + .fillMaxWidth() + .padding(padding), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + UserListView( + state = state.userListState, + onUserSelected = onUserSelected, + ) + + if (!state.userListState.isSearchActive) { + if (state.allUsers is Async.Success) { + LazyColumn(modifier = Modifier.fillMaxWidth(), state = rememberLazyListState()) { + item { + val memberCount = state.allUsers.state.count() + Text( + modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp), + text = pluralStringResource(id = R.plurals.screen_room_member_list_header_title, count = memberCount, memberCount), + style = ElementTextStyles.Regular.callout, + color = MaterialTheme.colorScheme.secondary, + textAlign = TextAlign.Start, + ) + } + items(state.allUsers.state) { matrixUser -> + SearchSingleUserResultItem( + modifier = Modifier.fillMaxWidth(), + matrixUser = matrixUser, + onClick = { onUserSelected(matrixUser) } + ) + } + } + } else if (state.allUsers.isLoading()) { + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + CircularProgressIndicator() + } + } + } + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun RoomMemberListTopBar( + modifier: Modifier = Modifier, + onBackPressed: () -> Unit = {}, +) { + CenterAlignedTopAppBar( + modifier = modifier, + title = { + Text( + text = stringResource(R.string.screen_room_details_people_title), + fontSize = 16.sp, + fontWeight = FontWeight.SemiBold, + ) + }, + navigationIcon = { BackButton(onClick = onBackPressed) }, + ) +} + +@Preview +@Composable +fun RoomMemberListLightPreview(@PreviewParameter(RoomMemberListStateProvider::class) state: RoomMemberListState) = + ElementPreviewLight { ContentToPreview(state) } + +@Preview +@Composable +fun RoomMemberListDarkPreview(@PreviewParameter(RoomMemberListStateProvider::class) state: RoomMemberListState) = + ElementPreviewDark { ContentToPreview(state) } + +@Composable +private fun ContentToPreview(state: RoomMemberListState) { + RoomMemberListView(state) +} diff --git a/features/roomdetails/impl/src/main/res/values/localazy.xml b/features/roomdetails/impl/src/main/res/values/localazy.xml index d373ed9bd1..82c0c1c418 100644 --- a/features/roomdetails/impl/src/main/res/values/localazy.xml +++ b/features/roomdetails/impl/src/main/res/values/localazy.xml @@ -1,5 +1,9 @@ + + "1 person" + "%1$d people" + "Messages are secured with locks. Only you and the recipients have the unique keys to unlock them." "Message encryption enabled" "Invite people" diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/RoomDetailsPresenterTests.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/RoomDetailsPresenterTests.kt index f77b606119..7679e1eb7d 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/RoomDetailsPresenterTests.kt +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/RoomDetailsPresenterTests.kt @@ -21,6 +21,7 @@ import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth import io.element.android.features.roomdetails.impl.RoomDetailsPresenter +import io.element.android.libraries.architecture.Async import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.room.RoomMember import io.element.android.libraries.matrix.test.A_ROOM_ID @@ -42,8 +43,25 @@ class RoomDetailsPresenterTests { Truth.assertThat(initialState.roomName).isEqualTo(room.name) Truth.assertThat(initialState.roomAvatarUrl).isEqualTo(room.avatarUrl) Truth.assertThat(initialState.roomTopic).isEqualTo(room.topic) - Truth.assertThat(initialState.memberCount).isEqualTo(room.members.count()) + Truth.assertThat(initialState.memberCount).isEqualTo(Async.Loading(null)) Truth.assertThat(initialState.isEncrypted).isEqualTo(room.isEncrypted) + + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - room member count is calculated asynchronously`() = runTest { + val room = aMatrixRoom() + val presenter = RoomDetailsPresenter(room) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + Truth.assertThat(initialState.memberCount).isEqualTo(Async.Loading(null)) + + val finalState = awaitItem() + Truth.assertThat(finalState.memberCount).isEqualTo(Async.Success(0)) } } @@ -56,6 +74,24 @@ class RoomDetailsPresenterTests { }.test { val initialState = awaitItem() Truth.assertThat(initialState.roomName).isEqualTo(room.displayName) + + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - can handle error while fetching member count`() = runTest { + val room = aMatrixRoom(name = null).apply { + givenFetchMemberResult(Result.failure(Throwable())) + } + val presenter = RoomDetailsPresenter(room) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + skipItems(1) + Truth.assertThat(awaitItem().memberCount).isInstanceOf(Async.Failure::class.java) + + cancelAndIgnoreRemainingEvents() } } } diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/RoomMemberListPresenterTests.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/RoomMemberListPresenterTests.kt new file mode 100644 index 0000000000..3564daa5f1 --- /dev/null +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/RoomMemberListPresenterTests.kt @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.roomdetails.members + +import app.cash.molecule.RecompositionClock +import app.cash.molecule.moleculeFlow +import app.cash.turbine.test +import com.google.common.truth.Truth +import io.element.android.features.roomdetails.impl.members.RoomMemberListPresenter +import io.element.android.features.userlist.api.SelectionMode +import io.element.android.features.userlist.api.MatrixUserDataSource +import io.element.android.features.userlist.api.UserListPresenter +import io.element.android.features.userlist.api.UserListPresenterArgs +import io.element.android.features.userlist.impl.DefaultUserListPresenter +import io.element.android.features.userlist.test.FakeMatrixUserDataSource +import io.element.android.libraries.architecture.Async +import io.element.android.libraries.matrix.ui.components.aMatrixUser +import kotlinx.coroutines.test.runTest +import okhttp3.internal.toImmutableList +import org.junit.Test + +class RoomMemberListPresenterTests { + + @Test + fun `present - search is done automatically on start, but is async`() = runTest { + val searchResult = listOf(aMatrixUser()) + val userListDataSource = FakeMatrixUserDataSource().apply { + givenSearchResult(searchResult) + } + val userListFactory = object : UserListPresenter.Factory { + override fun create(args: UserListPresenterArgs, dataSource: MatrixUserDataSource) = DefaultUserListPresenter(args, dataSource) + } + val presenter = RoomMemberListPresenter(userListFactory, userListDataSource) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + Truth.assertThat(initialState.allUsers).isInstanceOf(Async.Loading::class.java) + Truth.assertThat(initialState.userListState.isSearchActive).isFalse() + Truth.assertThat(initialState.userListState.searchResults).isEmpty() + Truth.assertThat(initialState.userListState.selectionMode).isEqualTo(SelectionMode.Single) + + val loadedState = awaitItem() + Truth.assertThat((loadedState.allUsers as? Async.Success)?.state).isEqualTo(searchResult.toImmutableList()) + } + } + +} diff --git a/features/selectusers/api/build.gradle.kts b/features/userlist/api/build.gradle.kts similarity index 93% rename from features/selectusers/api/build.gradle.kts rename to features/userlist/api/build.gradle.kts index d46ed2fbf1..7410de4224 100644 --- a/features/selectusers/api/build.gradle.kts +++ b/features/userlist/api/build.gradle.kts @@ -19,7 +19,7 @@ plugins { } android { - namespace = "io.element.android.features.selectusers.api" + namespace = "io.element.android.features.userlist.api" } dependencies { diff --git a/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/MatrixUserDataSource.kt b/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/MatrixUserDataSource.kt new file mode 100644 index 0000000000..08eddfd7e9 --- /dev/null +++ b/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/MatrixUserDataSource.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.userlist.api + +import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.ui.model.MatrixUser + +interface MatrixUserDataSource { + suspend fun search(query: String): List + suspend fun getProfile(userId: UserId): MatrixUser? +} diff --git a/features/selectusers/api/src/main/kotlin/io/element/android/features/selectusers/api/SelectUsersEvents.kt b/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/UserListEvents.kt similarity index 68% rename from features/selectusers/api/src/main/kotlin/io/element/android/features/selectusers/api/SelectUsersEvents.kt rename to features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/UserListEvents.kt index e0ee6ddf68..f648a14d74 100644 --- a/features/selectusers/api/src/main/kotlin/io/element/android/features/selectusers/api/SelectUsersEvents.kt +++ b/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/UserListEvents.kt @@ -14,13 +14,13 @@ * limitations under the License. */ -package io.element.android.features.selectusers.api +package io.element.android.features.userlist.api import io.element.android.libraries.matrix.ui.model.MatrixUser -sealed interface SelectUsersEvents { - data class UpdateSearchQuery(val query: String) : SelectUsersEvents - data class AddToSelection(val matrixUser: MatrixUser) : SelectUsersEvents - data class RemoveFromSelection(val matrixUser: MatrixUser) : SelectUsersEvents - data class OnSearchActiveChanged(val active: Boolean) : SelectUsersEvents +sealed interface UserListEvents { + data class UpdateSearchQuery(val query: String) : UserListEvents + data class AddToSelection(val matrixUser: MatrixUser) : UserListEvents + data class RemoveFromSelection(val matrixUser: MatrixUser) : UserListEvents + data class OnSearchActiveChanged(val active: Boolean) : UserListEvents } diff --git a/features/selectusers/api/src/main/kotlin/io/element/android/features/selectusers/api/SelectUsersPresenter.kt b/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/UserListPresenter.kt similarity index 76% rename from features/selectusers/api/src/main/kotlin/io/element/android/features/selectusers/api/SelectUsersPresenter.kt rename to features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/UserListPresenter.kt index be85455d09..c328efd44e 100644 --- a/features/selectusers/api/src/main/kotlin/io/element/android/features/selectusers/api/SelectUsersPresenter.kt +++ b/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/UserListPresenter.kt @@ -14,13 +14,13 @@ * limitations under the License. */ -package io.element.android.features.selectusers.api +package io.element.android.features.userlist.api import io.element.android.libraries.architecture.Presenter -interface SelectUsersPresenter : Presenter { +interface UserListPresenter : Presenter { interface Factory { - fun create(args: SelectUsersPresenterArgs): SelectUsersPresenter + fun create(args: UserListPresenterArgs, matrixUserDataSource: MatrixUserDataSource): UserListPresenter } } diff --git a/features/selectusers/api/src/main/kotlin/io/element/android/features/selectusers/api/SelectUsersPresenterArgs.kt b/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/UserListPresenterArgs.kt similarity index 88% rename from features/selectusers/api/src/main/kotlin/io/element/android/features/selectusers/api/SelectUsersPresenterArgs.kt rename to features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/UserListPresenterArgs.kt index 543e73b77e..9c8a40504b 100644 --- a/features/selectusers/api/src/main/kotlin/io/element/android/features/selectusers/api/SelectUsersPresenterArgs.kt +++ b/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/UserListPresenterArgs.kt @@ -14,9 +14,9 @@ * limitations under the License. */ -package io.element.android.features.selectusers.api +package io.element.android.features.userlist.api -data class SelectUsersPresenterArgs( +data class UserListPresenterArgs( val selectionMode: SelectionMode, ) diff --git a/features/selectusers/api/src/main/kotlin/io/element/android/features/selectusers/api/SelectUsersState.kt b/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/UserListState.kt similarity index 89% rename from features/selectusers/api/src/main/kotlin/io/element/android/features/selectusers/api/SelectUsersState.kt rename to features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/UserListState.kt index 2a1a2c48e3..80de1e991f 100644 --- a/features/selectusers/api/src/main/kotlin/io/element/android/features/selectusers/api/SelectUsersState.kt +++ b/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/UserListState.kt @@ -14,20 +14,20 @@ * limitations under the License. */ -package io.element.android.features.selectusers.api +package io.element.android.features.userlist.api import androidx.compose.foundation.lazy.LazyListState import io.element.android.libraries.matrix.ui.model.MatrixUser import kotlinx.collections.immutable.ImmutableList -data class SelectUsersState( +data class UserListState( val searchQuery: String, val searchResults: ImmutableList, val selectedUsers: ImmutableList, val selectedUsersListState: LazyListState, val isSearchActive: Boolean, val selectionMode: SelectionMode, - val eventSink: (SelectUsersEvents) -> Unit, + val eventSink: (UserListEvents) -> Unit, ) { val isMultiSelectionEnabled = selectionMode == SelectionMode.Multiple } diff --git a/features/selectusers/api/src/main/kotlin/io/element/android/features/selectusers/api/SelectUsersStateProvider.kt b/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/UserListStateProvider.kt similarity index 82% rename from features/selectusers/api/src/main/kotlin/io/element/android/features/selectusers/api/SelectUsersStateProvider.kt rename to features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/UserListStateProvider.kt index 93209a632c..d97a4537ed 100644 --- a/features/selectusers/api/src/main/kotlin/io/element/android/features/selectusers/api/SelectUsersStateProvider.kt +++ b/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/UserListStateProvider.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.selectusers.api +package io.element.android.features.userlist.api import androidx.compose.foundation.lazy.LazyListState import androidx.compose.ui.tooling.preview.PreviewParameterProvider @@ -22,25 +22,25 @@ import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.ui.model.MatrixUser import kotlinx.collections.immutable.persistentListOf -open class SelectUsersStateProvider : PreviewParameterProvider { - override val values: Sequence +open class UserListStateProvider : PreviewParameterProvider { + override val values: Sequence get() = sequenceOf( - aSelectUsersState(), - aSelectUsersState().copy( + aUserListState(), + aUserListState().copy( isSearchActive = false, selectedUsers = aListOfSelectedUsers(), selectionMode = SelectionMode.Multiple, ), - aSelectUsersState().copy(isSearchActive = true), - aSelectUsersState().copy(isSearchActive = true, searchQuery = "someone"), - aSelectUsersState().copy(isSearchActive = true, searchQuery = "someone", selectionMode = SelectionMode.Multiple), - aSelectUsersState().copy( + aUserListState().copy(isSearchActive = true), + aUserListState().copy(isSearchActive = true, searchQuery = "someone"), + aUserListState().copy(isSearchActive = true, searchQuery = "someone", selectionMode = SelectionMode.Multiple), + aUserListState().copy( isSearchActive = true, searchQuery = "@someone:matrix.org", selectedUsers = aListOfSelectedUsers(), searchResults = aListOfResults(), ), - aSelectUsersState().copy( + aUserListState().copy( isSearchActive = true, searchQuery = "@someone:matrix.org", selectionMode = SelectionMode.Multiple, @@ -50,7 +50,7 @@ open class SelectUsersStateProvider : PreviewParameterProvider ) } -fun aSelectUsersState() = SelectUsersState( +fun aUserListState() = UserListState( isSearchActive = false, searchQuery = "", searchResults = persistentListOf(), diff --git a/features/selectusers/api/src/main/kotlin/io/element/android/features/selectusers/api/SelectUsersView.kt b/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/UserListView.kt similarity index 92% rename from features/selectusers/api/src/main/kotlin/io/element/android/features/selectusers/api/SelectUsersView.kt rename to features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/UserListView.kt index 41a5360936..bc355a0a26 100644 --- a/features/selectusers/api/src/main/kotlin/io/element/android/features/selectusers/api/SelectUsersView.kt +++ b/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/UserListView.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.selectusers.api +package io.element.android.features.userlist.api import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -65,8 +65,8 @@ import kotlinx.collections.immutable.ImmutableList import io.element.android.libraries.ui.strings.R as StringR @Composable -fun SelectUsersView( - state: SelectUsersState, +fun UserListView( + state: UserListState, modifier: Modifier = Modifier, onUserSelected: (MatrixUser) -> Unit = {}, onUserDeselected: (MatrixUser) -> Unit = {}, @@ -82,14 +82,14 @@ fun SelectUsersView( selectedUsersListState = state.selectedUsersListState, active = state.isSearchActive, isMultiSelectionEnabled = state.isMultiSelectionEnabled, - onActiveChanged = { state.eventSink(SelectUsersEvents.OnSearchActiveChanged(it)) }, - onTextChanged = { state.eventSink(SelectUsersEvents.UpdateSearchQuery(it)) }, + onActiveChanged = { state.eventSink(UserListEvents.OnSearchActiveChanged(it)) }, + onTextChanged = { state.eventSink(UserListEvents.UpdateSearchQuery(it)) }, onUserSelected = { - state.eventSink(SelectUsersEvents.AddToSelection(it)) + state.eventSink(UserListEvents.AddToSelection(it)) onUserSelected(it) }, onUserDeselected = { - state.eventSink(SelectUsersEvents.RemoveFromSelection(it)) + state.eventSink(UserListEvents.RemoveFromSelection(it)) onUserDeselected(it) }, ) @@ -100,7 +100,7 @@ fun SelectUsersView( modifier = Modifier.padding(16.dp), selectedUsers = state.selectedUsers, onUserRemoved = { - state.eventSink(SelectUsersEvents.RemoveFromSelection(it)) + state.eventSink(UserListEvents.RemoveFromSelection(it)) onUserDeselected(it) }, ) @@ -297,15 +297,15 @@ fun SelectedUser( @Preview @Composable -internal fun SelectUsersViewLightPreview(@PreviewParameter(SelectUsersStateProvider::class) state: SelectUsersState) = +internal fun UserListViewLightPreview(@PreviewParameter(UserListStateProvider::class) state: UserListState) = ElementPreviewLight { ContentToPreview(state) } @Preview @Composable -internal fun SelectUsersViewDarkPreview(@PreviewParameter(SelectUsersStateProvider::class) state: SelectUsersState) = +internal fun UserListViewDarkPreview(@PreviewParameter(UserListStateProvider::class) state: UserListState) = ElementPreviewDark { ContentToPreview(state) } @Composable -private fun ContentToPreview(state: SelectUsersState) { - SelectUsersView(state = state) +private fun ContentToPreview(state: UserListState) { + UserListView(state = state) } diff --git a/features/selectusers/impl/build.gradle.kts b/features/userlist/impl/build.gradle.kts similarity index 92% rename from features/selectusers/impl/build.gradle.kts rename to features/userlist/impl/build.gradle.kts index baac7d2d2f..0eaca78a18 100644 --- a/features/selectusers/impl/build.gradle.kts +++ b/features/userlist/impl/build.gradle.kts @@ -23,7 +23,7 @@ plugins { } android { - namespace = "io.element.android.features.selectusers.impl" + namespace = "io.element.android.features.userlist.impl" } anvil { @@ -41,7 +41,7 @@ dependencies { implementation(projects.libraries.elementresources) implementation(projects.libraries.testtags) implementation(projects.libraries.uiStrings) - api(projects.features.selectusers.api) + api(projects.features.userlist.api) ksp(libs.showkase.processor) testImplementation(libs.test.junit) @@ -52,6 +52,7 @@ dependencies { testImplementation(libs.test.turbine) testImplementation(libs.test.mockk) testImplementation(projects.libraries.matrix.test) + testImplementation(projects.features.userlist.test) androidTestImplementation(libs.test.junitext) } diff --git a/features/selectusers/impl/src/main/kotlin/io/element/android/features/selectusers/impl/DefaultSelectUsersPresenter.kt b/features/userlist/impl/src/main/kotlin/io/element/android/features/userlist/impl/DefaultUserListPresenter.kt similarity index 73% rename from features/selectusers/impl/src/main/kotlin/io/element/android/features/selectusers/impl/DefaultSelectUsersPresenter.kt rename to features/userlist/impl/src/main/kotlin/io/element/android/features/userlist/impl/DefaultUserListPresenter.kt index e1135cd1a2..567d183e15 100644 --- a/features/selectusers/impl/src/main/kotlin/io/element/android/features/selectusers/impl/DefaultSelectUsersPresenter.kt +++ b/features/userlist/impl/src/main/kotlin/io/element/android/features/userlist/impl/DefaultUserListPresenter.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.selectusers.impl +package io.element.android.features.userlist.impl import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.rememberLazyListState @@ -31,10 +31,11 @@ import com.squareup.anvil.annotations.ContributesBinding import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject -import io.element.android.features.selectusers.api.SelectUsersEvents -import io.element.android.features.selectusers.api.SelectUsersPresenter -import io.element.android.features.selectusers.api.SelectUsersPresenterArgs -import io.element.android.features.selectusers.api.SelectUsersState +import io.element.android.features.userlist.api.MatrixUserDataSource +import io.element.android.features.userlist.api.UserListEvents +import io.element.android.features.userlist.api.UserListPresenterArgs +import io.element.android.features.userlist.api.UserListState +import io.element.android.features.userlist.api.UserListPresenter import io.element.android.libraries.di.SessionScope import io.element.android.libraries.matrix.api.core.MatrixPatterns import io.element.android.libraries.matrix.api.core.UserId @@ -45,18 +46,19 @@ import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch -class DefaultSelectUsersPresenter @AssistedInject constructor( - @Assisted val args: SelectUsersPresenterArgs, -) : SelectUsersPresenter { +class DefaultUserListPresenter @AssistedInject constructor( + @Assisted val args: UserListPresenterArgs, + @Assisted val matrixUserDataSource: MatrixUserDataSource, +) : UserListPresenter { @AssistedFactory @ContributesBinding(SessionScope::class) - interface DefaultSelectUsersFactory : SelectUsersPresenter.Factory { - override fun create(args: SelectUsersPresenterArgs): DefaultSelectUsersPresenter + interface DefaultUserListFactory : UserListPresenter.Factory { + override fun create(args: UserListPresenterArgs, matrixUserDataSource: MatrixUserDataSource): DefaultUserListPresenter } @Composable - override fun present(): SelectUsersState { + override fun present(): UserListState { val localCoroutineScope = rememberCoroutineScope() var isSearchActive by rememberSaveable { mutableStateOf(false) } val selectedUsers: MutableState> = remember { @@ -68,17 +70,17 @@ class DefaultSelectUsersPresenter @AssistedInject constructor( mutableStateOf(persistentListOf()) } - fun handleEvents(event: SelectUsersEvents) { + fun handleEvents(event: UserListEvents) { when (event) { - is SelectUsersEvents.OnSearchActiveChanged -> isSearchActive = event.active - is SelectUsersEvents.UpdateSearchQuery -> searchQuery = event.query - is SelectUsersEvents.AddToSelection -> { + is UserListEvents.OnSearchActiveChanged -> isSearchActive = event.active + is UserListEvents.UpdateSearchQuery -> searchQuery = event.query + is UserListEvents.AddToSelection -> { if (event.matrixUser !in selectedUsers.value) { selectedUsers.value = selectedUsers.value.plus(event.matrixUser).toImmutableList() } localCoroutineScope.scrollToFirstSelectedUser(selectedUsersListState) } - is SelectUsersEvents.RemoveFromSelection -> selectedUsers.value = selectedUsers.value.minus(event.matrixUser).toImmutableList() + is UserListEvents.RemoveFromSelection -> selectedUsers.value = selectedUsers.value.minus(event.matrixUser).toImmutableList() } } @@ -95,7 +97,7 @@ class DefaultSelectUsersPresenter @AssistedInject constructor( } } - return SelectUsersState( + return UserListState( searchQuery = searchQuery, searchResults = searchResults.value, selectedUsers = selectedUsers.value.reversed().toImmutableList(), @@ -106,11 +108,11 @@ class DefaultSelectUsersPresenter @AssistedInject constructor( ) } - private fun performSearch(query: String): ImmutableList { + private suspend fun performSearch(query: String): ImmutableList { val isMatrixId = MatrixPatterns.isUserId(query) - val results = mutableListOf()// TODO trigger /search request + val results = matrixUserDataSource.search(query).toMutableList() if (isMatrixId && results.none { it.id.value == query }) { - val getProfileResult: MatrixUser? = null // TODO trigger /profile request + val getProfileResult: MatrixUser? = matrixUserDataSource.getProfile(UserId(query)) val profile = getProfileResult ?: MatrixUser(UserId(query)) results.add(0, profile) } diff --git a/features/selectusers/impl/src/test/kotlin/io/element/android/features/selectusers/impl/DefaultSelectUsersPresenterTests.kt b/features/userlist/impl/src/test/kotlin/io/element/android/features/userlist/impl/DefaultUserListPresenterTests.kt similarity index 68% rename from features/selectusers/impl/src/test/kotlin/io/element/android/features/selectusers/impl/DefaultSelectUsersPresenterTests.kt rename to features/userlist/impl/src/test/kotlin/io/element/android/features/userlist/impl/DefaultUserListPresenterTests.kt index f5b0b43d0e..1cae186d56 100644 --- a/features/selectusers/impl/src/test/kotlin/io/element/android/features/selectusers/impl/DefaultSelectUsersPresenterTests.kt +++ b/features/userlist/impl/src/test/kotlin/io/element/android/features/userlist/impl/DefaultUserListPresenterTests.kt @@ -14,16 +14,17 @@ * limitations under the License. */ -package io.element.android.features.selectusers.impl +package io.element.android.features.userlist.impl import androidx.compose.foundation.lazy.LazyListState import app.cash.molecule.RecompositionClock import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth.assertThat -import io.element.android.features.selectusers.api.SelectUsersEvents -import io.element.android.features.selectusers.api.SelectUsersPresenterArgs -import io.element.android.features.selectusers.api.SelectionMode +import io.element.android.features.userlist.api.UserListEvents +import io.element.android.features.userlist.api.UserListPresenterArgs +import io.element.android.features.userlist.api.SelectionMode +import io.element.android.features.userlist.test.FakeMatrixUserDataSource import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.ui.components.aMatrixUser import io.element.android.libraries.matrix.ui.model.MatrixUser @@ -34,11 +35,16 @@ import kotlinx.coroutines.test.runTest import org.junit.Test @OptIn(ExperimentalCoroutinesApi::class) -class DefaultSelectUsersPresenterTests { +class DefaultUserListPresenterTests { + + private val userListDataSource = FakeMatrixUserDataSource() @Test fun `present - initial state for single selection`() = runTest { - val presenter = DefaultSelectUsersPresenter(SelectUsersPresenterArgs(SelectionMode.Single)) + val presenter = DefaultUserListPresenter( + UserListPresenterArgs(selectionMode = SelectionMode.Single), + userListDataSource + ) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { @@ -53,7 +59,10 @@ class DefaultSelectUsersPresenterTests { @Test fun `present - initial state for multiple selection`() = runTest { - val presenter = DefaultSelectUsersPresenter(SelectUsersPresenterArgs(SelectionMode.Multiple)) + val presenter = DefaultUserListPresenter( + UserListPresenterArgs(selectionMode = SelectionMode.Multiple), + userListDataSource + ) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { @@ -68,26 +77,29 @@ class DefaultSelectUsersPresenterTests { @Test fun `present - update search query`() = runTest { - val presenter = DefaultSelectUsersPresenter(SelectUsersPresenterArgs(SelectionMode.Single)) + val presenter = DefaultUserListPresenter( + UserListPresenterArgs(selectionMode = SelectionMode.Single), + userListDataSource + ) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { val initialState = awaitItem() - initialState.eventSink(SelectUsersEvents.OnSearchActiveChanged(true)) + initialState.eventSink(UserListEvents.OnSearchActiveChanged(true)) assertThat(awaitItem().isSearchActive).isTrue() val matrixIdQuery = "@name:matrix.org" - initialState.eventSink(SelectUsersEvents.UpdateSearchQuery(matrixIdQuery)) + initialState.eventSink(UserListEvents.UpdateSearchQuery(matrixIdQuery)) assertThat(awaitItem().searchQuery).isEqualTo(matrixIdQuery) assertThat(awaitItem().searchResults).containsExactly(MatrixUser(UserId(matrixIdQuery))) val notMatrixIdQuery = "name" - initialState.eventSink(SelectUsersEvents.UpdateSearchQuery(notMatrixIdQuery)) + initialState.eventSink(UserListEvents.UpdateSearchQuery(notMatrixIdQuery)) assertThat(awaitItem().searchQuery).isEqualTo(notMatrixIdQuery) assertThat(awaitItem().searchResults).isEmpty() - initialState.eventSink(SelectUsersEvents.OnSearchActiveChanged(false)) + initialState.eventSink(UserListEvents.OnSearchActiveChanged(false)) assertThat(awaitItem().isSearchActive).isFalse() } } @@ -97,7 +109,10 @@ class DefaultSelectUsersPresenterTests { mockkConstructor(LazyListState::class) coJustRun { anyConstructed().scrollToItem(index = any()) } - val presenter = DefaultSelectUsersPresenter(SelectUsersPresenterArgs(SelectionMode.Single)) + val presenter = DefaultUserListPresenter( + UserListPresenterArgs(selectionMode = SelectionMode.Single), + userListDataSource + ) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { @@ -108,23 +123,23 @@ class DefaultSelectUsersPresenterTests { val userABis = aMatrixUser("userA", "A") val userC = aMatrixUser("userC", "C") - initialState.eventSink(SelectUsersEvents.AddToSelection(userA)) + initialState.eventSink(UserListEvents.AddToSelection(userA)) assertThat(awaitItem().selectedUsers).containsExactly(userA) - initialState.eventSink(SelectUsersEvents.AddToSelection(userB)) + initialState.eventSink(UserListEvents.AddToSelection(userB)) // the last added user should be presented first assertThat(awaitItem().selectedUsers).containsExactly(userB, userA) - initialState.eventSink(SelectUsersEvents.AddToSelection(userABis)) - initialState.eventSink(SelectUsersEvents.AddToSelection(userC)) + initialState.eventSink(UserListEvents.AddToSelection(userABis)) + initialState.eventSink(UserListEvents.AddToSelection(userC)) // duplicated users should be ignored assertThat(awaitItem().selectedUsers).containsExactly(userC, userB, userA) - initialState.eventSink(SelectUsersEvents.RemoveFromSelection(userB)) + initialState.eventSink(UserListEvents.RemoveFromSelection(userB)) assertThat(awaitItem().selectedUsers).containsExactly(userC, userA) - initialState.eventSink(SelectUsersEvents.RemoveFromSelection(userA)) + initialState.eventSink(UserListEvents.RemoveFromSelection(userA)) assertThat(awaitItem().selectedUsers).containsExactly(userC) - initialState.eventSink(SelectUsersEvents.RemoveFromSelection(userC)) + initialState.eventSink(UserListEvents.RemoveFromSelection(userC)) assertThat(awaitItem().selectedUsers).isEmpty() } } diff --git a/features/userlist/test/build.gradle.kts b/features/userlist/test/build.gradle.kts new file mode 100644 index 0000000000..56ac66c154 --- /dev/null +++ b/features/userlist/test/build.gradle.kts @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// TODO: Remove once https://youtrack.jetbrains.com/issue/KTIJ-19369 is fixed +@Suppress("DSL_SCOPE_VIOLATION") +plugins { + id("io.element.android-library") +} + +android { + namespace = "io.element.android.features.userlist.test" +} + +dependencies { + implementation(projects.libraries.matrixui) + implementation(projects.libraries.matrix.api) + api(projects.features.userlist.api) + api(libs.coroutines.core) +} diff --git a/features/userlist/test/src/main/kotlin/io/element/android/features/userlist/test/FakeMatrixUserDataSource.kt b/features/userlist/test/src/main/kotlin/io/element/android/features/userlist/test/FakeMatrixUserDataSource.kt new file mode 100644 index 0000000000..db6297ec05 --- /dev/null +++ b/features/userlist/test/src/main/kotlin/io/element/android/features/userlist/test/FakeMatrixUserDataSource.kt @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.userlist.test + +import io.element.android.features.userlist.api.MatrixUserDataSource +import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.ui.model.MatrixUser + +class FakeMatrixUserDataSource : MatrixUserDataSource { + + private var searchResult: List = emptyList() + private var profile: MatrixUser? = null + + override suspend fun search(query: String): List = searchResult + + override suspend fun getProfile(userId: UserId): MatrixUser? = profile + + fun givenSearchResult(users: List) { + this.searchResult = users + } + + fun givenUserProfile(matrixUser: MatrixUser?) { + this.profile = matrixUser + } +} diff --git a/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/Async.kt b/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/Async.kt index 236ead4b0c..bb74dff2a9 100644 --- a/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/Async.kt +++ b/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/Async.kt @@ -57,3 +57,7 @@ suspend fun (suspend () -> Result).executeResult(state: MutableState Async.isLoading(): Boolean { + return this is Async.Loading +} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/PreferenceText.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/PreferenceText.kt index 7d8d219cbf..96dfdbaba7 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/PreferenceText.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/PreferenceText.kt @@ -25,7 +25,9 @@ import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width +import androidx.compose.foundation.progressSemantics import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.BugReport import androidx.compose.material3.MaterialTheme @@ -39,6 +41,7 @@ import androidx.compose.ui.unit.dp import io.element.android.libraries.designsystem.components.preferences.components.PreferenceIcon import io.element.android.libraries.designsystem.preview.ElementPreviewDark import io.element.android.libraries.designsystem.preview.ElementPreviewLight +import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator import io.element.android.libraries.designsystem.theme.components.Text @Composable @@ -47,6 +50,7 @@ fun PreferenceText( modifier: Modifier = Modifier, subtitle: String? = null, currentValue: String? = null, + loadingCurrentValue: Boolean = false, icon: ImageVector? = null, tintColor: Color? = null, onClick: () -> Unit = {}, @@ -56,11 +60,13 @@ fun PreferenceText( modifier = modifier .fillMaxWidth() .defaultMinSize(minHeight = minHeight) - .padding(end = preferencePaddingHorizontal) - .clickable { onClick() }, + .clickable { onClick() } + .padding(end = preferencePaddingHorizontal), ) { Row( - modifier = Modifier.fillMaxWidth().padding(vertical = preferencePaddingVertical) + modifier = Modifier + .fillMaxWidth() + .padding(vertical = preferencePaddingVertical) ) { PreferenceIcon(icon = icon, tintColor = tintColor) Column(modifier = Modifier @@ -88,7 +94,11 @@ fun PreferenceText( if (currentValue != null) { Text(currentValue, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.secondary) Spacer(Modifier.width(16.dp)) + } else if (loadingCurrentValue) { + CircularProgressIndicator(modifier = Modifier.progressSemantics().size(20.dp), strokeWidth = 2.dp) + Spacer(Modifier.width(16.dp)) } + } } } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt index 07d466d428..bd76d95b35 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt @@ -31,9 +31,11 @@ interface MatrixRoom: Closeable { val alternativeAliases: List val topic: String? val avatarUrl: String? - val members: List val isEncrypted: Boolean + suspend fun members() : List + suspend fun memberCount(): Int + fun syncUpdateFlow(): Flow fun timeline(): MatrixTimeline diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt index eb3bc1c7d2..291aa57392 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt @@ -17,6 +17,7 @@ package io.element.android.libraries.matrix.impl.room import io.element.android.libraries.core.coroutine.CoroutineDispatchers +import io.element.android.libraries.core.data.tryOrNull import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.room.MatrixRoom @@ -24,10 +25,12 @@ import io.element.android.libraries.matrix.api.room.RoomMember import io.element.android.libraries.matrix.api.timeline.MatrixTimeline import io.element.android.libraries.matrix.impl.timeline.RustMatrixTimeline import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.matrix.rustcomponents.sdk.Room import org.matrix.rustcomponents.sdk.SlidingSyncRoom @@ -43,10 +46,32 @@ class RustMatrixRoom( private val coroutineDispatchers: CoroutineDispatchers, ) : MatrixRoom { + private var loadMembersJob: Job? = null + private var cachedMembers: List = emptyList() + + override suspend fun members(): List { + return cachedMembers.ifEmpty { + if (loadMembersJob == null) { + loadMembersJob = coroutineScope.launch(coroutineDispatchers.io) { + cachedMembers = tryOrNull { + innerRoom.members().map(RoomMemberMapper::map) + } ?: emptyList() + } + } + loadMembersJob?.join() + loadMembersJob = null + cachedMembers + } + } + + override suspend fun memberCount(): Int { + return members().size + } + override fun syncUpdateFlow(): Flow { return slidingSyncUpdateFlow .filter { - it.rooms.contains(innerRoom.id()) + it.rooms.contains(roomId.value) } .map { System.currentTimeMillis() @@ -95,9 +120,6 @@ class RustMatrixRoom( return innerRoom.avatarUrl() } - override val members: List - get() = innerRoom.members().map(RoomMemberMapper::map) - override val isEncrypted: Boolean get() = innerRoom.isEncrypted() diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustMatrixTimeline.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustMatrixTimeline.kt index 15a79c3586..e942f31b76 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustMatrixTimeline.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustMatrixTimeline.kt @@ -87,14 +87,6 @@ class RustMatrixTimeline( override fun initialize() { Timber.v("Init timeline for room ${matrixRoom.roomId}") - coroutineScope.launch { - matrixRoom.fetchMembers() - .onFailure { - Timber.e(it, "Fail to fetch members for room ${matrixRoom.roomId}") - }.onSuccess { - Timber.v("Success fetching members for room ${matrixRoom.roomId}") - } - } coroutineScope.launch { val result = addListener(innerTimelineListener) result diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt index 32e73121c8..b8ea695f47 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt @@ -34,13 +34,18 @@ class FakeMatrixRoom( override val displayName: String = "", override val topic: String? = null, override val avatarUrl: String? = null, - override val members: List = emptyList(), override val isEncrypted: Boolean = false, override val alias: String? = null, override val alternativeAliases: List = emptyList(), + private val members: List = emptyList(), private val matrixTimeline: MatrixTimeline = FakeMatrixTimeline(), ) : MatrixRoom { + private var fetchMemberResult: Result = Result.success(Unit) + + var areMembersFetched: Boolean = false + private set + override fun syncUpdateFlow(): Flow { return emptyFlow() } @@ -50,7 +55,11 @@ class FakeMatrixRoom( } override suspend fun fetchMembers(): Result { - return Result.success(Unit) + return fetchMemberResult.also { result -> + if (result.isSuccess) { + areMembersFetched = true + } + } } override suspend fun userDisplayName(userId: String): Result { @@ -61,6 +70,18 @@ class FakeMatrixRoom( TODO("Not yet implemented") } + override suspend fun members(): List { + return members + } + + override suspend fun memberCount(): Int { + if (fetchMemberResult.isSuccess) { + return members.count() + } else { + throw fetchMemberResult.exceptionOrNull()!! + } + } + override suspend fun sendMessage(message: String): Result { delay(100) return Result.success(Unit) @@ -94,4 +115,8 @@ class FakeMatrixRoom( } override fun close() = Unit + + fun givenFetchMemberResult(result: Result) { + fetchMemberResult = result + } } diff --git a/libraries/ui-strings/src/main/res/values-ro/translations.xml b/libraries/ui-strings/src/main/res/values-ro/translations.xml new file mode 100644 index 0000000000..6878d7aab4 --- /dev/null +++ b/libraries/ui-strings/src/main/res/values-ro/translations.xml @@ -0,0 +1,10 @@ + + + "Confirmare" + "Creați o cameră" + "Gata" + "OK" + "Raportează conținutul" + "Începe discuția" + "Vezi sursa" + \ No newline at end of file diff --git a/libraries/ui-strings/src/main/res/values/localazy.xml b/libraries/ui-strings/src/main/res/values/localazy.xml index a28d8bb7bc..7ae90b9b53 100644 --- a/libraries/ui-strings/src/main/res/values/localazy.xml +++ b/libraries/ui-strings/src/main/res/values/localazy.xml @@ -48,6 +48,7 @@ "About" "Audio" "Bubbles" + "Creating room…" "Decryption error" "Developer options" "(edited)" @@ -122,11 +123,19 @@ "Rageshake to report bug" "You seem to be shaking the phone in frustration. Would you like to open the bug report screen?" - "Reporting this message will send it’s unique ‘event ID’ to the administrator of your homeserver. If messages in this room are encrypted, your homeserver administrator will not be able to read the message text or view any files or images." + "This message will be reported to your homeserver’s administrator. They will not be able to read any encrypted messages." "Reason for reporting this content" "This is the beginning of %1$s." "This is the beginning of this conversation." "New" + "Block" + "Blocked users will not be able to send you messages and all message by them will be hidden. You can reverse this action anytime." + "Block user" + "Unblock" + "On unblocking the user, you will be able to see all messages by them again." + "Unblock user" + "Block user" + "Check if you want to hide all current and future messages from this user" "Block" "Blocked users will not be able to send you messages and all message by them will be hidden. You can reverse this action anytime." "Block user" diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members_null_DefaultGroup_RoomMemberListDarkPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members_null_DefaultGroup_RoomMemberListDarkPreview_0_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..27f36f9248 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members_null_DefaultGroup_RoomMemberListDarkPreview_0_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6893108aabbf06f43af90f17b6b1a8a521312559f407647ce08db06ecb9a8f84 +size 22033 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members_null_DefaultGroup_RoomMemberListDarkPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members_null_DefaultGroup_RoomMemberListDarkPreview_0_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..32d8a266d5 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members_null_DefaultGroup_RoomMemberListDarkPreview_0_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7e1f7feb544a84e6e66f995683038abb9e7465583fade4e77c254a4264d03b9f +size 11985 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members_null_DefaultGroup_RoomMemberListLightPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members_null_DefaultGroup_RoomMemberListLightPreview_0_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..b493c070d2 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members_null_DefaultGroup_RoomMemberListLightPreview_0_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3e7b5bd916d4d3067b5013400b4ac864fab560e96fcb75ec895684021a00b8ba +size 21808 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members_null_DefaultGroup_RoomMemberListLightPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members_null_DefaultGroup_RoomMemberListLightPreview_0_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..9ef4e1ee89 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members_null_DefaultGroup_RoomMemberListLightPreview_0_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:309c0c0a650435cfbc0825025f6f852bdb51899ff9d37cf4cf9db00d56fb1176 +size 11933 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview_0_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview_0_null_4,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..a3331e0b02 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview_0_null_4,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9fe9b98fbdd3fd9c7789463f14345f6f65f9a18eedf2229d14c62a3bee6711e3 +size 70058 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview_0_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview_0_null_4,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..2c0511cb21 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview_0_null_4,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:48f138bc8a63ac25152491c974c68d6e024ded04b973f174fb5b9586523217ca +size 64593 diff --git a/tools/localazy/config.json b/tools/localazy/config.json index 781807d057..155a42836a 100644 --- a/tools/localazy/config.json +++ b/tools/localazy/config.json @@ -61,7 +61,9 @@ { "name": ":features:roomdetails:impl", "includeRegex": [ - "screen_room_details_.*" + "screen_room_details_.*", + "screen_room_member_list_.*", + "screen_dm_details_.*" ] } ]