Merge branch 'develop' into renovate/org.owasp.dependencycheck-10.x
This commit is contained in:
commit
a011571d12
281 changed files with 3290 additions and 1976 deletions
13
.github/workflows/build.yml
vendored
13
.github/workflows/build.yml
vendored
|
|
@ -9,7 +9,7 @@ on:
|
|||
|
||||
# Enrich gradle.properties for CI/CD
|
||||
env:
|
||||
GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx7g -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError" -Dkotlin.incremental=false -XX:+UseG1GC
|
||||
GRADLE_OPTS: -Dorg.gradle.jvmargs=-Xmx9g -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError -XX:+UseG1GC -Dkotlin.daemon.jvm.options=-Xmx4g -Dkotlin.incremental=false
|
||||
CI_GRADLE_ARG_PROPERTIES: --stacktrace -Dsonar.gradle.skipCompile=true
|
||||
|
||||
jobs:
|
||||
|
|
@ -39,20 +39,13 @@ jobs:
|
|||
uses: gradle/actions/setup-gradle@v4
|
||||
with:
|
||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||
- name: Assemble debug Gplay APK
|
||||
- name: Assemble debug APKs
|
||||
if: ${{ matrix.variant == 'debug' }}
|
||||
env:
|
||||
ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }}
|
||||
ELEMENT_ANDROID_MAPTILER_LIGHT_MAP_ID: ${{ secrets.MAPTILER_LIGHT_MAP_ID }}
|
||||
ELEMENT_ANDROID_MAPTILER_DARK_MAP_ID: ${{ secrets.MAPTILER_DARK_MAP_ID }}
|
||||
run: ./gradlew :app:assembleGplayDebug -PallWarningsAsErrors=true $CI_GRADLE_ARG_PROPERTIES
|
||||
- name: Assemble debug Fdroid APK
|
||||
if: ${{ matrix.variant == 'debug' }}
|
||||
env:
|
||||
ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }}
|
||||
ELEMENT_ANDROID_MAPTILER_LIGHT_MAP_ID: ${{ secrets.MAPTILER_LIGHT_MAP_ID }}
|
||||
ELEMENT_ANDROID_MAPTILER_DARK_MAP_ID: ${{ secrets.MAPTILER_DARK_MAP_ID }}
|
||||
run: ./gradlew app:assembleFDroidDebug -PallWarningsAsErrors=true $CI_GRADLE_ARG_PROPERTIES
|
||||
run: ./gradlew :app:assembleGplayDebug app:assembleFDroidDebug -PallWarningsAsErrors=true $CI_GRADLE_ARG_PROPERTIES
|
||||
- name: Upload debug APKs
|
||||
if: ${{ matrix.variant == 'debug' }}
|
||||
uses: actions/upload-artifact@v4
|
||||
|
|
|
|||
4
.github/workflows/build_enterprise.yml
vendored
4
.github/workflows/build_enterprise.yml
vendored
|
|
@ -9,7 +9,7 @@ on:
|
|||
|
||||
# Enrich gradle.properties for CI/CD
|
||||
env:
|
||||
GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx7g -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError" -Dkotlin.incremental=false -XX:+UseG1GC
|
||||
GRADLE_OPTS: -Dorg.gradle.jvmargs=-Xmx9g -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError -XX:+UseG1GC -Dkotlin.daemon.jvm.options=-Xmx4g -Dkotlin.incremental=false
|
||||
CI_GRADLE_ARG_PROPERTIES: --stacktrace -Dsonar.gradle.skipCompile=true
|
||||
|
||||
jobs:
|
||||
|
|
@ -61,7 +61,7 @@ jobs:
|
|||
name: elementx-enterprise-debug
|
||||
path: |
|
||||
app/build/outputs/apk/gplay/debug/*-universal-debug.apk
|
||||
- name: Compile release sources
|
||||
- name: Compile nightly and release sources
|
||||
if: ${{ matrix.variant == 'release' }}
|
||||
run: ./gradlew compileReleaseSources -PallWarningsAsErrors=true $CI_GRADLE_ARG_PROPERTIES
|
||||
- name: Compile nightly sources
|
||||
|
|
|
|||
4
.github/workflows/maestro.yml
vendored
4
.github/workflows/maestro.yml
vendored
|
|
@ -8,7 +8,7 @@ on:
|
|||
|
||||
# Enrich gradle.properties for CI/CD
|
||||
env:
|
||||
GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx7g -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError" -Dkotlin.incremental=false -XX:+UseG1GC
|
||||
GRADLE_OPTS: -Dorg.gradle.jvmargs=-Xmx9g -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError -XX:+UseG1GC -Dkotlin.daemon.jvm.options=-Xmx4g -Dkotlin.incremental=false
|
||||
CI_GRADLE_ARG_PROPERTIES: --stacktrace --no-daemon -Dsonar.gradle.skipCompile=true
|
||||
|
||||
jobs:
|
||||
|
|
@ -79,7 +79,7 @@ jobs:
|
|||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: elementx-apk-maestro
|
||||
- uses: mobile-dev-inc/action-maestro-cloud@v1.8.1
|
||||
- uses: mobile-dev-inc/action-maestro-cloud@v1.9.1
|
||||
if: (github.event_name == 'pull_request' && github.event.pull_request.fork == null) || github.event_name == 'workflow_dispatch'
|
||||
with:
|
||||
api-key: ${{ secrets.MAESTRO_CLOUD_API_KEY }}
|
||||
|
|
|
|||
2
.github/workflows/nightly.yml
vendored
2
.github/workflows/nightly.yml
vendored
|
|
@ -7,7 +7,7 @@ on:
|
|||
- cron: "0 4 * * *"
|
||||
|
||||
env:
|
||||
GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx6g -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError" -Dkotlin.incremental=false -XX:+UseG1GC
|
||||
GRADLE_OPTS: -Dorg.gradle.jvmargs=-Xmx9g -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError -XX:+UseG1GC -Dkotlin.daemon.jvm.options=-Xmx4g -Dkotlin.incremental=false
|
||||
CI_GRADLE_ARG_PROPERTIES: --stacktrace --no-daemon -Dsonar.gradle.skipCompile=true
|
||||
|
||||
jobs:
|
||||
|
|
|
|||
2
.github/workflows/nightlyReports.yml
vendored
2
.github/workflows/nightlyReports.yml
vendored
|
|
@ -8,7 +8,7 @@ on:
|
|||
|
||||
# Enrich gradle.properties for CI/CD
|
||||
env:
|
||||
GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx7g -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError" -Dkotlin.incremental=false -XX:+UseG1GC
|
||||
GRADLE_OPTS: -Dorg.gradle.jvmargs=-Xmx9g -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError -XX:+UseG1GC -Dkotlin.daemon.jvm.options=-Xmx4g -Dkotlin.incremental=false
|
||||
CI_GRADLE_ARG_PROPERTIES: --stacktrace -Dsonar.gradle.skipCompile=true
|
||||
|
||||
jobs:
|
||||
|
|
|
|||
2
.github/workflows/nightly_enterprise.yml
vendored
2
.github/workflows/nightly_enterprise.yml
vendored
|
|
@ -7,7 +7,7 @@ on:
|
|||
- cron: "0 4 * * *"
|
||||
|
||||
env:
|
||||
GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx6g -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError" -Dkotlin.incremental=false -XX:+UseG1GC
|
||||
GRADLE_OPTS: -Dorg.gradle.jvmargs=-Xmx9g -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError -XX:+UseG1GC -Dkotlin.daemon.jvm.options=-Xmx4g -Dkotlin.incremental=false
|
||||
CI_GRADLE_ARG_PROPERTIES: --stacktrace --no-daemon -Dsonar.gradle.skipCompile=true
|
||||
|
||||
jobs:
|
||||
|
|
|
|||
2
.github/workflows/quality.yml
vendored
2
.github/workflows/quality.yml
vendored
|
|
@ -9,7 +9,7 @@ on:
|
|||
|
||||
# Enrich gradle.properties for CI/CD
|
||||
env:
|
||||
GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx6g -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError" -Dkotlin.incremental=false -XX:+UseG1GC
|
||||
GRADLE_OPTS: -Dorg.gradle.jvmargs=-Xmx9g -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError -XX:+UseG1GC -Dkotlin.daemon.jvm.options=-Xmx4g -Dkotlin.incremental=false
|
||||
CI_GRADLE_ARG_PROPERTIES: --stacktrace --no-daemon -Dsonar.gradle.skipCompile=true
|
||||
|
||||
jobs:
|
||||
|
|
|
|||
2
.github/workflows/recordScreenshots.yml
vendored
2
.github/workflows/recordScreenshots.yml
vendored
|
|
@ -7,7 +7,7 @@ on:
|
|||
|
||||
# Enrich gradle.properties for CI/CD
|
||||
env:
|
||||
GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx5g -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError" -Dkotlin.incremental=false -XX:+UseG1GC -Dsonar.gradle.skipCompile=true
|
||||
GRADLE_OPTS: -Dorg.gradle.jvmargs=-Xmx9g -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError -XX:+UseG1GC -Dkotlin.daemon.jvm.options=-Xmx4g -Dkotlin.incremental=false -Dsonar.gradle.skipCompile=true
|
||||
|
||||
jobs:
|
||||
record:
|
||||
|
|
|
|||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
|
|
@ -7,7 +7,7 @@ on:
|
|||
|
||||
# Enrich gradle.properties for CI/CD
|
||||
env:
|
||||
GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx6g -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError" -Dkotlin.incremental=false -XX:+UseG1GC
|
||||
GRADLE_OPTS: -Dorg.gradle.jvmargs=-Xmx9g -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError -XX:+UseG1GC -Dkotlin.daemon.jvm.options=-Xmx4g -Dkotlin.incremental=false
|
||||
CI_GRADLE_ARG_PROPERTIES: --stacktrace --no-daemon -Dsonar.gradle.skipCompile=true
|
||||
|
||||
jobs:
|
||||
|
|
|
|||
14
.github/workflows/sonar.yml
vendored
14
.github/workflows/sonar.yml
vendored
|
|
@ -9,7 +9,7 @@ on:
|
|||
|
||||
# Enrich gradle.properties for CI/CD
|
||||
env:
|
||||
GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx6g -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError" -Dkotlin.incremental=false -XX:+UseG1GC
|
||||
GRADLE_OPTS: -Dorg.gradle.jvmargs=-Xmx9g -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError -XX:+UseG1GC -Dkotlin.daemon.jvm.options=-Xmx4g -Dkotlin.incremental=false
|
||||
CI_GRADLE_ARG_PROPERTIES: --stacktrace --warn -Dsonar.gradle.skipCompile=true
|
||||
GROUP: ${{ format('sonar-{0}', github.ref) }}
|
||||
|
||||
|
|
@ -36,16 +36,8 @@ jobs:
|
|||
uses: gradle/actions/setup-gradle@v4
|
||||
with:
|
||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||
- name: Build Gplay Debug
|
||||
run: ./gradlew :app:assembleGplayDebug $CI_GRADLE_ARG_PROPERTIES
|
||||
- name: Build Fdroid Debug
|
||||
run: ./gradlew :app:assembleFdroidDebug $CI_GRADLE_ARG_PROPERTIES
|
||||
- name: Build Sample
|
||||
run: ./gradlew :samples:minimal:assembleDebug $CI_GRADLE_ARG_PROPERTIES
|
||||
- name: Build library fixtures
|
||||
run: ./gradlew assembleDebug createFullJarDebugTestFixtures $CI_GRADLE_ARG_PROPERTIES
|
||||
- name: Build app fixtures
|
||||
run: ./gradlew :app:createFullJarGplayDebugTestFixtures $CI_GRADLE_ARG_PROPERTIES
|
||||
- name: Build debug code and test fixtures
|
||||
run: ./gradlew assembleDebug createFullJarDebugTestFixtures :app:createFullJarGplayDebugTestFixtures $CI_GRADLE_ARG_PROPERTIES
|
||||
- name: 🔊 Publish results to Sonar
|
||||
env:
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
|
|
|
|||
2
.github/workflows/tests.yml
vendored
2
.github/workflows/tests.yml
vendored
|
|
@ -9,7 +9,7 @@ on:
|
|||
|
||||
# Enrich gradle.properties for CI/CD
|
||||
env:
|
||||
GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx7g -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError" -Dkotlin.incremental=false -XX:+UseG1GC
|
||||
GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx7g -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError" -Dkotlin.incremental=false -Dkotlin.daemon.jvm.options=-Xmx2g -XX:+UseG1GC
|
||||
CI_GRADLE_ARG_PROPERTIES: --stacktrace -Dsonar.gradle.skipCompile=true
|
||||
|
||||
jobs:
|
||||
|
|
|
|||
46
CHANGES.md
46
CHANGES.md
|
|
@ -1,3 +1,49 @@
|
|||
Changes in Element X v0.5.2 (2024-09-05)
|
||||
=========================================
|
||||
|
||||
### 🙌 Improvements
|
||||
* [Identity reset] Remove instruction to reset identity on another client. by @bmarty in https://github.com/element-hq/element-x-android/pull/3355
|
||||
* Redact message on displayed notification by @bmarty in https://github.com/element-hq/element-x-android/pull/3320
|
||||
* Add a way to sign out when the user is asked to verify the session. by @bmarty in https://github.com/element-hq/element-x-android/pull/3359
|
||||
* Add banner entry point to set up recovery by @bmarty in https://github.com/element-hq/element-x-android/pull/3360
|
||||
* Replace OSS licenses plugin with Licensee and some manually done UI. by @bmarty in https://github.com/element-hq/element-x-android/pull/3381
|
||||
|
||||
### 🐛 Bugfixes
|
||||
* Small fixes around logging out. by @bmarty in https://github.com/element-hq/element-x-android/pull/3356
|
||||
* Ensure starting PinUnlockActivity does not crash the application. by @bmarty in https://github.com/element-hq/element-x-android/pull/3369
|
||||
* Use the right colors for `@room` mention pills by @jmartinesp in https://github.com/element-hq/element-x-android/pull/3376
|
||||
* Fix avatar sometimes not loading by @bmarty in https://github.com/element-hq/element-x-android/pull/3366
|
||||
* Make pinned events required state in SlidingSync by @jmartinesp in https://github.com/element-hq/element-x-android/pull/3385
|
||||
* Make sure to save the tokens the Client might return when its session is restored by @jmartinesp in https://github.com/element-hq/element-x-android/pull/3378
|
||||
* Fix Element Call closing automatically on API 34 by @jmartinesp in https://github.com/element-hq/element-x-android/pull/3402
|
||||
|
||||
### 🗣 Translations
|
||||
* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/3373
|
||||
|
||||
### 🧱 Build
|
||||
* Try adding a memory limit for the kotlin compiler by @jmartinesp in https://github.com/element-hq/element-x-android/pull/3377
|
||||
|
||||
### Dependency upgrades
|
||||
* Update dependency com.google.testparameterinjector:test-parameter-injector to v1.17 by @renovate in https://github.com/element-hq/element-x-android/pull/3357
|
||||
* Update dependencyAnalysis to v2.0.1 by @renovate in https://github.com/element-hq/element-x-android/pull/3362
|
||||
* Update android.gradle.plugin to v8.6.0 by @renovate in https://github.com/element-hq/element-x-android/pull/3363
|
||||
* Update dependency io.nlopez.compose.rules:detekt to v0.4.11 by @renovate in https://github.com/element-hq/element-x-android/pull/3364
|
||||
* Update dependency com.posthog:posthog-android to v3.6.0 by @renovate in https://github.com/element-hq/element-x-android/pull/3358
|
||||
* Update mobile-dev-inc/action-maestro-cloud action to v1.9.1 by @renovate in https://github.com/element-hq/element-x-android/pull/3367
|
||||
* Update dependency com.posthog:posthog-android to v3.6.1 by @renovate in https://github.com/element-hq/element-x-android/pull/3368
|
||||
* Update dependency org.matrix.rustcomponents:sdk-android to v0.2.41 by @renovate in https://github.com/element-hq/element-x-android/pull/3384
|
||||
* Rust sdk : update to 0.2.42 by @ganfra in https://github.com/element-hq/element-x-android/pull/3393
|
||||
* Update dependency com.android.tools:desugar_jdk_libs to v2.1.2 by @renovate in https://github.com/element-hq/element-x-android/pull/3350
|
||||
* Update dependency com.sigpwned:emoji4j-core to v15.1.2 by @renovate in https://github.com/element-hq/element-x-android/pull/3396
|
||||
|
||||
### Others
|
||||
* Release : use a different concurrency group for enterprise build by @ganfra in https://github.com/element-hq/element-x-android/pull/3351
|
||||
* Provide distinct cache directory to the Rust SDK. by @bmarty in https://github.com/element-hq/element-x-android/pull/3370
|
||||
* Remove the migration screen by @bmarty in https://github.com/element-hq/element-x-android/pull/3389
|
||||
* Unified push endpoint: do not fallback to default endpoint in case of failure and add troubleshoot test. by @bmarty in https://github.com/element-hq/element-x-android/pull/3388
|
||||
* Skip device verification screen when creating a new account using OIDC by @bmarty in https://github.com/element-hq/element-x-android/pull/3395
|
||||
* Big emoji-only messages by @frebib in https://github.com/element-hq/element-x-android/pull/3295
|
||||
|
||||
Changes in Element X v0.5.1 (2024-08-28)
|
||||
=========================================
|
||||
|
||||
|
|
|
|||
|
|
@ -17,15 +17,19 @@
|
|||
@file:Suppress("UnstableApiUsage")
|
||||
|
||||
import com.android.build.api.variant.FilterConfiguration.FilterType.ABI
|
||||
import com.android.build.gradle.internal.tasks.factory.dependsOn
|
||||
import com.android.build.gradle.tasks.GenerateBuildConfig
|
||||
import extension.AssetCopyTask
|
||||
import extension.GitBranchNameValueSource
|
||||
import extension.GitRevisionValueSource
|
||||
import extension.allEnterpriseImpl
|
||||
import extension.allFeaturesImpl
|
||||
import extension.allLibrariesImpl
|
||||
import extension.allServicesImpl
|
||||
import extension.gitBranchName
|
||||
import extension.gitRevision
|
||||
import extension.koverDependencies
|
||||
import extension.locales
|
||||
import extension.setupKover
|
||||
import java.util.Locale
|
||||
|
||||
plugins {
|
||||
id("io.element.android-compose-application")
|
||||
|
|
@ -36,7 +40,8 @@ plugins {
|
|||
id(libs.plugins.firebaseAppDistribution.get().pluginId)
|
||||
alias(libs.plugins.knit)
|
||||
id("kotlin-parcelize")
|
||||
id("com.google.android.gms.oss-licenses-plugin")
|
||||
alias(libs.plugins.licensee)
|
||||
alias(libs.plugins.kotlin.serialization)
|
||||
// To be able to update the firebase.xml files, uncomment and build the project
|
||||
// id("com.google.gms.google-services")
|
||||
}
|
||||
|
|
@ -61,9 +66,6 @@ android {
|
|||
abiFilters += listOf("armeabi-v7a", "x86", "arm64-v8a", "x86_64")
|
||||
}
|
||||
|
||||
buildConfigField("String", "GIT_REVISION", "\"${gitRevision()}\"")
|
||||
buildConfigField("String", "GIT_BRANCH_NAME", "\"${gitBranchName()}\"")
|
||||
|
||||
// Ref: https://developer.android.com/studio/build/configure-apk-splits.html#configure-abi-split
|
||||
splits {
|
||||
// Configures multiple APKs based on ABI.
|
||||
|
|
@ -215,6 +217,9 @@ androidComponents {
|
|||
output.versionCode.set((output.versionCode.orNull ?: 0) * 10 + abiCode)
|
||||
}
|
||||
}
|
||||
|
||||
val reportingExtension: ReportingExtension = project.extensions.getByType(ReportingExtension::class.java)
|
||||
configureLicensesTasks(reportingExtension)
|
||||
}
|
||||
|
||||
// Knit
|
||||
|
|
@ -259,8 +264,6 @@ dependencies {
|
|||
// Comment to not include unified push in the project
|
||||
implementation(projects.libraries.pushproviders.unifiedpush)
|
||||
|
||||
"gplayImplementation"(libs.play.services.oss.licenses)
|
||||
|
||||
implementation(libs.appyx.core)
|
||||
implementation(libs.androidx.splash)
|
||||
implementation(libs.androidx.core)
|
||||
|
|
@ -291,3 +294,51 @@ dependencies {
|
|||
|
||||
koverDependencies()
|
||||
}
|
||||
|
||||
tasks.withType<GenerateBuildConfig>().configureEach {
|
||||
outputs.upToDateWhen { false }
|
||||
val gitRevision = providers.of(GitRevisionValueSource::class.java) {}.get()
|
||||
val gitBranchName = providers.of(GitBranchNameValueSource::class.java) {}.get()
|
||||
android.defaultConfig.buildConfigField("String", "GIT_REVISION", "\"$gitRevision\"")
|
||||
android.defaultConfig.buildConfigField("String", "GIT_BRANCH_NAME", "\"$gitBranchName\"")
|
||||
}
|
||||
|
||||
licensee {
|
||||
allow("Apache-2.0")
|
||||
allow("MIT")
|
||||
allow("GPL-2.0-with-classpath-exception")
|
||||
allow("BSD-2-Clause")
|
||||
allowUrl("https://opensource.org/licenses/MIT")
|
||||
allowUrl("https://developer.android.com/studio/terms.html")
|
||||
allowUrl("http://openjdk.java.net/legal/gplv2+ce.html")
|
||||
allowUrl("https://www.zetetic.net/sqlcipher/license/")
|
||||
allowUrl("https://jsoup.org/license")
|
||||
allowUrl("https://asm.ow2.io/license.html")
|
||||
ignoreDependencies("com.github.matrix-org", "matrix-analytics-events")
|
||||
}
|
||||
|
||||
fun Project.configureLicensesTasks(reportingExtension: ReportingExtension) {
|
||||
androidComponents {
|
||||
onVariants { variant ->
|
||||
val capitalizedVariantName = variant.name.replaceFirstChar {
|
||||
if (it.isLowerCase()) {
|
||||
it.titlecase(Locale.getDefault())
|
||||
} else {
|
||||
it.toString()
|
||||
}
|
||||
}
|
||||
val artifactsFile = reportingExtension.file("licensee/android$capitalizedVariantName/artifacts.json")
|
||||
|
||||
val copyArtifactsTask =
|
||||
project.tasks.register<AssetCopyTask>("copy${capitalizedVariantName}LicenseeReportToAssets") {
|
||||
inputFile.set(artifactsFile)
|
||||
targetFileName.set("licensee-artifacts.json")
|
||||
}
|
||||
variant.sources.assets?.addGeneratedSourceDirectory(
|
||||
copyArtifactsTask,
|
||||
AssetCopyTask::outputDirectory,
|
||||
)
|
||||
copyArtifactsTask.dependsOn("licenseeAndroid$capitalizedVariantName")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +0,0 @@
|
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<application>
|
||||
<activity
|
||||
android:name="com.google.android.gms.oss.licenses.OssLicensesMenuActivity"
|
||||
android:theme="@style/Theme.OssLicenses" />
|
||||
<activity
|
||||
android:name="com.google.android.gms.oss.licenses.OssLicensesActivity"
|
||||
android:theme="@style/Theme.OssLicenses" />
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2024 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
|
||||
*
|
||||
* https://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.x.licenses
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import com.google.android.gms.oss.licenses.OssLicensesMenuActivity
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.features.preferences.api.OpenSourceLicensesProvider
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import javax.inject.Inject
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class OssOpenSourcesLicensesProvider @Inject constructor() : OpenSourceLicensesProvider {
|
||||
override val hasOpenSourceLicenses: Boolean = true
|
||||
|
||||
override fun navigateToOpenSourceLicenses(activity: Activity) {
|
||||
val title = activity.getString(CommonStrings.common_open_source_licenses)
|
||||
OssLicensesMenuActivity.setActivityTitle(title)
|
||||
activity.startActivity(Intent(activity, OssLicensesMenuActivity::class.java))
|
||||
}
|
||||
}
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
~ Copyright (c) 2024 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
|
||||
~
|
||||
~ https://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.
|
||||
-->
|
||||
|
||||
<resources>
|
||||
|
||||
<!-- Use a few colors from compoundColorsDark -->
|
||||
<!-- DarkColorTokens.colorThemeBg -->
|
||||
<color name="colorThemeBg">#FF101317</color>
|
||||
<!-- DarkColorTokens.colorGray1400 -->
|
||||
<color name="textPrimary">#FFEBEEF2</color>
|
||||
<!-- DarkColorTokens.colorGray900 -->
|
||||
<color name="textSecondary">#ff808994</color>
|
||||
<!-- DarkColorTokens.colorBlue900 -->
|
||||
<color name="textLinkExternal">#FF4187EB</color>
|
||||
|
||||
<bool name="windowLightStatusBar">false</bool>
|
||||
<bool name="windowLightNavigationBar">false</bool>
|
||||
|
||||
</resources>
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
~ Copyright (c) 2024 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
|
||||
~
|
||||
~ https://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.
|
||||
-->
|
||||
|
||||
<resources>
|
||||
|
||||
<style name="Theme.OssLicenses.Light.v27" parent="Base.Theme.OssLicenses">
|
||||
<item name="android:windowLightNavigationBar">@bool/windowLightNavigationBar</item>
|
||||
</style>
|
||||
|
||||
<style name="Theme.OssLicenses" parent="Theme.OssLicenses.Light.v27"/>
|
||||
|
||||
</resources>
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
~ Copyright (c) 2024 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
|
||||
~
|
||||
~ https://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.
|
||||
-->
|
||||
|
||||
<resources>
|
||||
|
||||
<!-- Use a few colors from compoundColorsLight -->
|
||||
<!-- LightColorTokens.colorThemeBg -->
|
||||
<color name="colorThemeBg">#FFFFFFFF</color>
|
||||
<!-- LightColorTokens.colorGray1400 -->
|
||||
<color name="textPrimary">#FF1B1D22</color>
|
||||
<!-- LightColorTokens.colorGray900 -->
|
||||
<color name="textSecondary">#FF656D77</color>
|
||||
<!-- LightColorTokens.colorBlue900 -->
|
||||
<color name="textLinkExternal">#FF0467DD</color>
|
||||
|
||||
<bool name="windowLightStatusBar">true</bool>
|
||||
<bool name="windowLightNavigationBar">true</bool>
|
||||
|
||||
</resources>
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
~ Copyright (c) 2024 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
|
||||
~
|
||||
~ https://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.
|
||||
-->
|
||||
|
||||
<resources>
|
||||
|
||||
<style name="NoElevationToolbar" parent="Widget.MaterialComponents.Toolbar">
|
||||
<item name="android:elevation">0dp</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
~ Copyright (c) 2024 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
|
||||
~
|
||||
~ https://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.
|
||||
-->
|
||||
|
||||
<resources>
|
||||
|
||||
<style name="Base.Theme.OssLicenses" parent="Theme.MaterialComponents.DayNight">
|
||||
<!-- Background of title bar -->
|
||||
<item name="colorPrimary">@color/colorThemeBg</item>
|
||||
<!-- Background of the screen -->
|
||||
<item name="android:colorBackground">@color/colorThemeBg</item>
|
||||
<!-- Text of the licenses -->
|
||||
<item name="android:textColor">@color/textSecondary</item>
|
||||
<!-- Link text color -->
|
||||
<item name="android:textColorLink">@color/textLinkExternal</item>
|
||||
<!-- Title, back button and license item text color -->
|
||||
<item name="android:textColorPrimary">@color/textPrimary</item>
|
||||
<!-- Background of status bar -->
|
||||
<item name="android:statusBarColor">@color/colorThemeBg</item>
|
||||
<item name="android:windowLightStatusBar">@bool/windowLightStatusBar</item>
|
||||
<!-- Background of navigation bar -->
|
||||
<item name="android:navigationBarColor">@color/colorThemeBg</item>
|
||||
<!-- Try to remove Toolbar elevation, but it does not work :/ -->
|
||||
<item name="toolbarStyle">@style/NoElevationToolbar</item>
|
||||
</style>
|
||||
|
||||
<style name="Theme.OssLicenses" parent="Base.Theme.OssLicenses" />
|
||||
|
||||
</resources>
|
||||
|
|
@ -1,11 +1,7 @@
|
|||
import com.google.devtools.ksp.gradle.KspTask
|
||||
import org.apache.tools.ant.taskdefs.optional.ReplaceRegExp
|
||||
|
||||
buildscript {
|
||||
dependencies {
|
||||
classpath(libs.kotlin.gradle.plugin)
|
||||
classpath(libs.gms.google.services)
|
||||
classpath(libs.oss.licenses.plugin)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -202,24 +198,6 @@ subprojects {
|
|||
tasks.findByName("recordPaparazziRelease")?.dependsOn(removeOldScreenshotsTask)
|
||||
}
|
||||
|
||||
// Workaround for https://github.com/airbnb/Showkase/issues/335
|
||||
subprojects {
|
||||
tasks.withType<KspTask> {
|
||||
doLast {
|
||||
fileTree(layout.buildDirectory).apply { include("**/*ShowkaseExtension*.kt") }.files.forEach { file ->
|
||||
ReplaceRegExp().apply {
|
||||
setMatch("^public fun Showkase.getMetadata")
|
||||
setReplace("@Suppress(\"DEPRECATION\") public fun Showkase.getMetadata")
|
||||
setFlags("g")
|
||||
setByLine(true)
|
||||
setFile(file)
|
||||
execute()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
subprojects {
|
||||
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
|
||||
kotlinOptions {
|
||||
|
|
|
|||
2
fastlane/metadata/android/en-US/changelogs/40005020.txt
Normal file
2
fastlane/metadata/android/en-US/changelogs/40005020.txt
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
Main changes in this version: mainly bug fixes.
|
||||
Full changelog: https://github.com/element-hq/element-x-android/releases
|
||||
|
|
@ -42,6 +42,7 @@ import androidx.core.app.PictureInPictureModeChangedInfo
|
|||
import androidx.core.content.IntentCompat
|
||||
import androidx.core.util.Consumer
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import io.element.android.features.call.api.CallType
|
||||
import io.element.android.features.call.impl.DefaultElementCallEntryPoint
|
||||
import io.element.android.features.call.impl.di.CallBindings
|
||||
|
|
@ -54,6 +55,7 @@ import io.element.android.features.call.impl.utils.CallIntentDataParser
|
|||
import io.element.android.libraries.architecture.bindings
|
||||
import io.element.android.libraries.designsystem.theme.ElementThemeApp
|
||||
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
|
|
@ -128,12 +130,14 @@ class ElementCallActivity :
|
|||
private fun ListenToAndroidEvents(pipState: PictureInPictureState) {
|
||||
val pipEventSink by rememberUpdatedState(pipState.eventSink)
|
||||
DisposableEffect(Unit) {
|
||||
val onUserLeaveHintListener = Runnable {
|
||||
val listener = Runnable {
|
||||
pipEventSink(PictureInPictureEvents.EnterPictureInPicture)
|
||||
}
|
||||
addOnUserLeaveHintListener(onUserLeaveHintListener)
|
||||
lifecycleScope.launch {
|
||||
addOnUserLeaveHintListener(listener)
|
||||
}
|
||||
onDispose {
|
||||
removeOnUserLeaveHintListener(onUserLeaveHintListener)
|
||||
removeOnUserLeaveHintListener(listener)
|
||||
}
|
||||
}
|
||||
DisposableEffect(Unit) {
|
||||
|
|
|
|||
|
|
@ -3,5 +3,5 @@
|
|||
<string name="call_foreground_service_channel_title_android">"Chamada em curso"</string>
|
||||
<string name="call_foreground_service_message_android">"Toca para voltar à chamada"</string>
|
||||
<string name="call_foreground_service_title_android">"☎️ Chamada em curso"</string>
|
||||
<string name="screen_incoming_call_subtitle_android">"A receber chamada da Element "</string>
|
||||
<string name="screen_incoming_call_subtitle_android">"A receber chamada da Element"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ class FtueSessionVerificationFlowNode @AssistedInject constructor(
|
|||
private val secureBackupEntryPoint: SecureBackupEntryPoint,
|
||||
) : BaseFlowNode<FtueSessionVerificationFlowNode.NavTarget>(
|
||||
backstack = BackStack(
|
||||
initialElement = NavTarget.Root,
|
||||
initialElement = NavTarget.Root(showDeviceVerifiedScreen = false),
|
||||
savedStateMap = buildContext.savedStateMap,
|
||||
),
|
||||
buildContext = buildContext,
|
||||
|
|
@ -54,7 +54,7 @@ class FtueSessionVerificationFlowNode @AssistedInject constructor(
|
|||
) {
|
||||
sealed interface NavTarget : Parcelable {
|
||||
@Parcelize
|
||||
data object Root : NavTarget
|
||||
data class Root(val showDeviceVerifiedScreen: Boolean) : NavTarget
|
||||
|
||||
@Parcelize
|
||||
data object EnterRecoveryKey : NavTarget
|
||||
|
|
@ -71,7 +71,7 @@ class FtueSessionVerificationFlowNode @AssistedInject constructor(
|
|||
override fun onDone() {
|
||||
lifecycleScope.launch {
|
||||
// Move to the completed state view in the verification flow
|
||||
backstack.newRoot(NavTarget.Root)
|
||||
backstack.newRoot(NavTarget.Root(showDeviceVerifiedScreen = true))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -80,6 +80,7 @@ class FtueSessionVerificationFlowNode @AssistedInject constructor(
|
|||
return when (navTarget) {
|
||||
is NavTarget.Root -> {
|
||||
verifySessionEntryPoint.nodeBuilder(this, buildContext)
|
||||
.params(VerifySessionEntryPoint.Params(navTarget.showDeviceVerifiedScreen))
|
||||
.callback(object : VerifySessionEntryPoint.Callback {
|
||||
override fun onEnterRecoveryKey() {
|
||||
backstack.push(NavTarget.EnterRecoveryKey)
|
||||
|
|
|
|||
|
|
@ -14,15 +14,15 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
plugins {
|
||||
id("io.element.android-library")
|
||||
id("io.element.android-compose-library")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "io.element.android.features.roomlist.test"
|
||||
namespace = "io.element.android.features.licenses.api"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.features.roomlist.api)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
implementation(projects.tests.testutils)
|
||||
implementation(projects.libraries.architecture)
|
||||
implementation(projects.libraries.designsystem)
|
||||
implementation(projects.libraries.uiStrings)
|
||||
}
|
||||
|
|
@ -14,11 +14,11 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.preferences.api
|
||||
package io.element.android.features.licenses.api
|
||||
|
||||
import android.app.Activity
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
|
||||
interface OpenSourceLicensesProvider {
|
||||
val hasOpenSourceLicenses: Boolean
|
||||
fun navigateToOpenSourceLicenses(activity: Activity)
|
||||
interface OpenSourceLicensesEntryPoint {
|
||||
fun getNode(node: Node, buildContext: BuildContext): Node
|
||||
}
|
||||
49
features/licenses/impl/build.gradle.kts
Normal file
49
features/licenses/impl/build.gradle.kts
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
plugins {
|
||||
id("io.element.android-compose-library")
|
||||
id("kotlin-parcelize")
|
||||
alias(libs.plugins.anvil)
|
||||
alias(libs.plugins.kotlin.serialization)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "io.element.android.features.licenses.impl"
|
||||
}
|
||||
|
||||
anvil {
|
||||
generateDaggerFactories.set(true)
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.anvilannotations)
|
||||
anvil(projects.anvilcodegen)
|
||||
implementation(libs.serialization.json)
|
||||
implementation(projects.libraries.architecture)
|
||||
implementation(projects.libraries.designsystem)
|
||||
implementation(projects.libraries.core)
|
||||
implementation(projects.libraries.uiStrings)
|
||||
api(projects.features.licenses.api)
|
||||
testImplementation(libs.test.junit)
|
||||
testImplementation(libs.coroutines.test)
|
||||
testImplementation(libs.coroutines.core)
|
||||
testImplementation(libs.molecule.runtime)
|
||||
testImplementation(libs.test.truth)
|
||||
testImplementation(libs.test.turbine)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
testImplementation(projects.tests.testutils)
|
||||
}
|
||||
|
|
@ -14,19 +14,19 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.x.licenses
|
||||
package io.element.android.features.licenses.impl
|
||||
|
||||
import android.app.Activity
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.features.preferences.api.OpenSourceLicensesProvider
|
||||
import io.element.android.features.licenses.api.OpenSourceLicensesEntryPoint
|
||||
import io.element.android.libraries.architecture.createNode
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import javax.inject.Inject
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class FdroidOpenSourceLicensesProvider @Inject constructor() : OpenSourceLicensesProvider {
|
||||
override val hasOpenSourceLicenses: Boolean = false
|
||||
|
||||
override fun navigateToOpenSourceLicenses(activity: Activity) {
|
||||
error("Not supported, please ensure that hasOpenSourcesLicenses is true before calling this method")
|
||||
class DefaultOpenSourcesLicensesEntryPoint @Inject constructor() : OpenSourceLicensesEntryPoint {
|
||||
override fun getNode(node: Node, buildContext: BuildContext): Node {
|
||||
return node.createNode<DependenciesFlowNode>(buildContext)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
/*
|
||||
* Copyright (c) 2024 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
|
||||
*
|
||||
* https://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.licenses.impl
|
||||
|
||||
import android.os.Parcelable
|
||||
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 com.bumble.appyx.navmodel.backstack.BackStack
|
||||
import com.bumble.appyx.navmodel.backstack.operation.push
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import io.element.android.anvilannotations.ContributesNode
|
||||
import io.element.android.features.licenses.impl.details.DependenciesDetailsNode
|
||||
import io.element.android.features.licenses.impl.list.DependencyLicensesListNode
|
||||
import io.element.android.features.licenses.impl.model.DependencyLicenseItem
|
||||
import io.element.android.libraries.architecture.BackstackView
|
||||
import io.element.android.libraries.architecture.BaseFlowNode
|
||||
import io.element.android.libraries.architecture.createNode
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@ContributesNode(AppScope::class)
|
||||
class DependenciesFlowNode @AssistedInject constructor(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
) : BaseFlowNode<DependenciesFlowNode.NavTarget>(
|
||||
backstack = BackStack(
|
||||
initialElement = NavTarget.LicensesList,
|
||||
savedStateMap = buildContext.savedStateMap,
|
||||
),
|
||||
buildContext = buildContext,
|
||||
plugins = plugins,
|
||||
) {
|
||||
sealed interface NavTarget : Parcelable {
|
||||
@Parcelize
|
||||
data object LicensesList : NavTarget
|
||||
|
||||
@Parcelize
|
||||
data class LicenseDetails(val license: DependencyLicenseItem) : NavTarget
|
||||
}
|
||||
|
||||
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||
return when (navTarget) {
|
||||
is NavTarget.LicensesList -> {
|
||||
val callback = object : DependencyLicensesListNode.Callback {
|
||||
override fun onOpenLicense(license: DependencyLicenseItem) {
|
||||
backstack.push(NavTarget.LicenseDetails(license))
|
||||
}
|
||||
}
|
||||
createNode<DependencyLicensesListNode>(buildContext, listOf(callback))
|
||||
}
|
||||
is NavTarget.LicenseDetails -> {
|
||||
createNode<DependenciesDetailsNode>(buildContext, listOf(DependenciesDetailsNode.Inputs(navTarget.license)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
BackstackView(modifier)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* Copyright (c) 2024 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
|
||||
*
|
||||
* https://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.licenses.impl
|
||||
|
||||
import android.content.Context
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.features.licenses.impl.model.DependencyLicenseItem
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.di.ApplicationContext
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.decodeFromStream
|
||||
import javax.inject.Inject
|
||||
|
||||
interface LicensesProvider {
|
||||
suspend fun provides(): List<DependencyLicenseItem>
|
||||
}
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class AssetLicensesProvider @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
private val dispatchers: CoroutineDispatchers,
|
||||
) : LicensesProvider {
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
override suspend fun provides(): List<DependencyLicenseItem> {
|
||||
return withContext(dispatchers.io) {
|
||||
context.assets.open("licensee-artifacts.json").use { inputStream ->
|
||||
val json = Json {
|
||||
ignoreUnknownKeys = true
|
||||
explicitNulls = false
|
||||
}
|
||||
json.decodeFromStream<List<DependencyLicenseItem>>(inputStream)
|
||||
.sortedBy { it.safeName.lowercase() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* Copyright (c) 2024 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
|
||||
*
|
||||
* https://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.licenses.impl.details
|
||||
|
||||
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.features.licenses.impl.model.DependencyLicenseItem
|
||||
import io.element.android.libraries.architecture.NodeInputs
|
||||
import io.element.android.libraries.architecture.inputs
|
||||
import io.element.android.libraries.di.AppScope
|
||||
|
||||
@ContributesNode(AppScope::class)
|
||||
class DependenciesDetailsNode @AssistedInject constructor(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
) : Node(
|
||||
buildContext = buildContext,
|
||||
plugins = plugins
|
||||
) {
|
||||
data class Inputs(
|
||||
val licenseItem: DependencyLicenseItem,
|
||||
) : NodeInputs
|
||||
|
||||
private val licenseItem = inputs<Inputs>().licenseItem
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
DependenciesDetailsView(
|
||||
modifier = modifier,
|
||||
licenseItem = licenseItem,
|
||||
onBack = ::navigateUp
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
/*
|
||||
* Copyright (c) 2024 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
|
||||
*
|
||||
* https://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.licenses.impl.details
|
||||
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import io.element.android.features.licenses.impl.list.aDependencyLicenseItem
|
||||
import io.element.android.features.licenses.impl.model.DependencyLicenseItem
|
||||
import io.element.android.libraries.designsystem.components.ClickableLinkText
|
||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.theme.components.ListItem
|
||||
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun DependenciesDetailsView(
|
||||
licenseItem: DependencyLicenseItem,
|
||||
onBack: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Scaffold(
|
||||
modifier = modifier,
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text(text = licenseItem.safeName) },
|
||||
navigationIcon = { BackButton(onClick = onBack) },
|
||||
)
|
||||
},
|
||||
) { contentPadding ->
|
||||
LazyColumn(
|
||||
modifier = Modifier.padding(contentPadding),
|
||||
) {
|
||||
val licenses = licenseItem.licenses.orEmpty() +
|
||||
licenseItem.unknownLicenses.orEmpty()
|
||||
items(licenses) { license ->
|
||||
val text = buildString {
|
||||
if (license.name != null) {
|
||||
append(license.name)
|
||||
append("\n")
|
||||
append("\n")
|
||||
}
|
||||
if (license.url != null) {
|
||||
append(license.url)
|
||||
}
|
||||
}
|
||||
ListItem(
|
||||
headlineContent = {
|
||||
ClickableLinkText(
|
||||
text = text,
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun DependenciesDetailsViewPreview() = ElementPreview {
|
||||
DependenciesDetailsView(
|
||||
licenseItem = aDependencyLicenseItem(),
|
||||
onBack = {}
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
* Copyright (c) 2024 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
|
||||
*
|
||||
* https://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.licenses.impl.list
|
||||
|
||||
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 com.bumble.appyx.core.plugin.plugins
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import io.element.android.anvilannotations.ContributesNode
|
||||
import io.element.android.features.licenses.impl.model.DependencyLicenseItem
|
||||
import io.element.android.libraries.di.AppScope
|
||||
|
||||
@ContributesNode(AppScope::class)
|
||||
class DependencyLicensesListNode @AssistedInject constructor(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
private val presenter: DependencyLicensesListPresenter,
|
||||
) : Node(
|
||||
buildContext = buildContext,
|
||||
plugins = plugins
|
||||
) {
|
||||
interface Callback : Plugin {
|
||||
fun onOpenLicense(license: DependencyLicenseItem)
|
||||
}
|
||||
|
||||
private fun onOpenLicense(license: DependencyLicenseItem) {
|
||||
plugins<Callback>()
|
||||
.forEach { it.onOpenLicense(license) }
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
val state = presenter.present()
|
||||
DependencyLicensesListView(
|
||||
state = state,
|
||||
onBackClick = ::navigateUp,
|
||||
onOpenLicense = ::onOpenLicense,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* Copyright (c) 2024 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
|
||||
*
|
||||
* https://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.licenses.impl.list
|
||||
|
||||
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.features.licenses.impl.LicensesProvider
|
||||
import io.element.android.features.licenses.impl.model.DependencyLicenseItem
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
import javax.inject.Inject
|
||||
|
||||
class DependencyLicensesListPresenter @Inject constructor(
|
||||
private val licensesProvider: LicensesProvider,
|
||||
) : Presenter<DependencyLicensesListState> {
|
||||
@Composable
|
||||
override fun present(): DependencyLicensesListState {
|
||||
var licenses by remember {
|
||||
mutableStateOf<AsyncData<ImmutableList<DependencyLicenseItem>>>(AsyncData.Loading())
|
||||
}
|
||||
LaunchedEffect(Unit) {
|
||||
runCatching {
|
||||
licenses = AsyncData.Success(licensesProvider.provides().toPersistentList())
|
||||
}.onFailure {
|
||||
licenses = AsyncData.Failure(it)
|
||||
}
|
||||
}
|
||||
return DependencyLicensesListState(licenses = licenses)
|
||||
}
|
||||
}
|
||||
|
|
@ -14,13 +14,12 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.preferences.impl.about
|
||||
package io.element.android.features.licenses.impl.list
|
||||
|
||||
import android.app.Activity
|
||||
import io.element.android.features.preferences.api.OpenSourceLicensesProvider
|
||||
import io.element.android.features.licenses.impl.model.DependencyLicenseItem
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
class FakeOpenSourceLicensesProvider(
|
||||
override val hasOpenSourceLicenses: Boolean,
|
||||
) : OpenSourceLicensesProvider {
|
||||
override fun navigateToOpenSourceLicenses(activity: Activity) = Unit
|
||||
}
|
||||
data class DependencyLicensesListState(
|
||||
val licenses: AsyncData<ImmutableList<DependencyLicenseItem>>,
|
||||
)
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* Copyright (c) 2024 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
|
||||
*
|
||||
* https://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.licenses.impl.list
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.features.licenses.impl.model.DependencyLicenseItem
|
||||
import io.element.android.features.licenses.impl.model.License
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
open class DependencyLicensesListStateProvider : PreviewParameterProvider<DependencyLicensesListState> {
|
||||
override val values: Sequence<DependencyLicensesListState>
|
||||
get() = sequenceOf(
|
||||
DependencyLicensesListState(
|
||||
licenses = AsyncData.Loading()
|
||||
),
|
||||
DependencyLicensesListState(
|
||||
licenses = AsyncData.Failure(Exception("Failed to load licenses"))
|
||||
),
|
||||
DependencyLicensesListState(
|
||||
licenses = AsyncData.Success(
|
||||
persistentListOf(
|
||||
aDependencyLicenseItem(),
|
||||
aDependencyLicenseItem(name = null),
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
internal fun aDependencyLicenseItem(
|
||||
name: String? = "A dependency",
|
||||
) = DependencyLicenseItem(
|
||||
groupId = "org.some.group",
|
||||
artifactId = "a-dependency",
|
||||
version = "1.0.0",
|
||||
name = name,
|
||||
licenses = listOf(
|
||||
License(
|
||||
identifier = "Apache 2.0",
|
||||
name = "Apache 2.0",
|
||||
url = "https://www.apache.org/licenses/LICENSE-2.0"
|
||||
)
|
||||
),
|
||||
unknownLicenses = listOf(),
|
||||
scm = null,
|
||||
)
|
||||
|
|
@ -0,0 +1,118 @@
|
|||
/*
|
||||
* Copyright (c) 2024 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
|
||||
*
|
||||
* https://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.licenses.impl.list
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
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.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.features.licenses.impl.model.DependencyLicenseItem
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
|
||||
import io.element.android.libraries.designsystem.theme.components.ListItem
|
||||
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun DependencyLicensesListView(
|
||||
state: DependencyLicensesListState,
|
||||
onBackClick: () -> Unit,
|
||||
onOpenLicense: (DependencyLicenseItem) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Scaffold(
|
||||
modifier = modifier,
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text(text = stringResource(CommonStrings.common_open_source_licenses)) },
|
||||
navigationIcon = { BackButton(onClick = onBackClick) },
|
||||
)
|
||||
},
|
||||
) { contentPadding ->
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.padding(contentPadding)
|
||||
.padding(horizontal = 16.dp)
|
||||
) {
|
||||
when (state.licenses) {
|
||||
is AsyncData.Failure -> item {
|
||||
Text(
|
||||
text = stringResource(CommonStrings.common_error),
|
||||
modifier = Modifier.padding(16.dp)
|
||||
)
|
||||
}
|
||||
AsyncData.Uninitialized,
|
||||
is AsyncData.Loading -> item {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 64.dp)
|
||||
) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.align(Alignment.Center)
|
||||
)
|
||||
}
|
||||
}
|
||||
is AsyncData.Success -> items(state.licenses.data) { license ->
|
||||
ListItem(
|
||||
headlineContent = { Text(license.safeName) },
|
||||
supportingContent = {
|
||||
Text(
|
||||
buildString {
|
||||
append(license.groupId)
|
||||
append(":")
|
||||
append(license.artifactId)
|
||||
append(":")
|
||||
append(license.version)
|
||||
}
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
onOpenLicense(license)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun DependencyLicensesListViewPreview(
|
||||
@PreviewParameter(DependencyLicensesListStateProvider::class) state: DependencyLicensesListState
|
||||
) = ElementPreview {
|
||||
DependencyLicensesListView(
|
||||
state = state,
|
||||
onBackClick = {},
|
||||
onOpenLicense = {},
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* Copyright (c) 2024 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
|
||||
*
|
||||
* https://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.licenses.impl.model
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.IgnoredOnParcel
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
@Parcelize
|
||||
data class DependencyLicenseItem(
|
||||
val groupId: String,
|
||||
val artifactId: String,
|
||||
val version: String,
|
||||
@SerialName("spdxLicenses")
|
||||
val licenses: List<License>?,
|
||||
val unknownLicenses: List<License>?,
|
||||
val name: String?,
|
||||
val scm: Scm?,
|
||||
) : Parcelable {
|
||||
@IgnoredOnParcel
|
||||
val safeName = name?.takeIf { name -> name != "null" } ?: "$groupId:$artifactId"
|
||||
}
|
||||
|
||||
@Serializable
|
||||
@Parcelize
|
||||
data class License(
|
||||
val identifier: String?,
|
||||
val name: String?,
|
||||
val url: String?,
|
||||
) : Parcelable
|
||||
|
||||
@Serializable
|
||||
@Parcelize
|
||||
data class Scm(
|
||||
val url: String,
|
||||
) : Parcelable
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
/*
|
||||
* Copyright (c) 2024 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
|
||||
*
|
||||
* https://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.licenses.impl.list
|
||||
|
||||
import app.cash.molecule.RecompositionMode
|
||||
import app.cash.molecule.moleculeFlow
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.licenses.impl.model.DependencyLicenseItem
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.tests.testutils.WarmUpRule
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
class DependencyLicensesListPresenterTest {
|
||||
@get:Rule
|
||||
val warmUpRule = WarmUpRule()
|
||||
|
||||
@Test
|
||||
fun `present - initial state, no licenses`() = runTest {
|
||||
val presenter = createPresenter { emptyList() }
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.licenses).isInstanceOf(AsyncData.Loading::class.java)
|
||||
val finalState = awaitItem()
|
||||
assertThat(finalState.licenses.isSuccess()).isTrue()
|
||||
assertThat(finalState.licenses.dataOrNull()).isEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - initial state, one license`() = runTest {
|
||||
val anItem = aDependencyLicenseItem()
|
||||
val presenter = createPresenter {
|
||||
listOf(anItem)
|
||||
}
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.licenses).isInstanceOf(AsyncData.Loading::class.java)
|
||||
val finalState = awaitItem()
|
||||
assertThat(finalState.licenses.isSuccess()).isTrue()
|
||||
assertThat(finalState.licenses.dataOrNull()!!.size).isEqualTo(1)
|
||||
assertThat(finalState.licenses.dataOrNull()!!.get(0)).isEqualTo(anItem)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createPresenter(
|
||||
provideResult: () -> List<DependencyLicenseItem>
|
||||
) = DependencyLicensesListPresenter(
|
||||
licensesProvider = FakeLicensesProvider(provideResult),
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* Copyright (c) 2024 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
|
||||
*
|
||||
* https://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.licenses.impl.list
|
||||
|
||||
import io.element.android.features.licenses.impl.LicensesProvider
|
||||
import io.element.android.features.licenses.impl.model.DependencyLicenseItem
|
||||
import io.element.android.tests.testutils.lambda.lambdaError
|
||||
|
||||
class FakeLicensesProvider(
|
||||
private val provideResult: () -> List<DependencyLicenseItem> = { lambdaError() }
|
||||
) : LicensesProvider {
|
||||
override suspend fun provides(): List<DependencyLicenseItem> {
|
||||
return provideResult()
|
||||
}
|
||||
}
|
||||
|
|
@ -18,9 +18,9 @@ package io.element.android.features.lockscreen.impl.unlock.di
|
|||
|
||||
import com.squareup.anvil.annotations.ContributesTo
|
||||
import io.element.android.features.lockscreen.impl.unlock.activity.PinUnlockActivity
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import io.element.android.libraries.di.AppScope
|
||||
|
||||
@ContributesTo(SessionScope::class)
|
||||
@ContributesTo(AppScope::class)
|
||||
interface PinUnlockBindings {
|
||||
fun inject(activity: PinUnlockActivity)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ sealed class ChangeServerError : Throwable() {
|
|||
|
||||
companion object {
|
||||
fun from(error: Throwable): ChangeServerError = when (error) {
|
||||
is AuthenticationException.SlidingSyncNotAvailable -> SlidingSyncAlert
|
||||
is AuthenticationException.SlidingSyncVersion -> SlidingSyncAlert
|
||||
else -> Error(R.string.screen_change_server_error_invalid_homeserver)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ class ErrorFormatterTest {
|
|||
|
||||
@Test
|
||||
fun `loginError - invalid auth error returns unknown error message`() {
|
||||
val error = AuthenticationException.SlidingSyncNotAvailable("Some message. Also contains M_FORBIDDEN, but won't be parsed")
|
||||
val error = AuthenticationException.SlidingSyncVersion("Some message. Also contains M_FORBIDDEN, but won't be parsed")
|
||||
assertThat(loginError(error)).isEqualTo(CommonStrings.error_unknown)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -17,27 +17,25 @@
|
|||
package io.element.android.features.logout.impl
|
||||
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import io.element.android.features.logout.api.LogoutUseCase
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.matrix.api.MatrixClientProvider
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
|
||||
import javax.inject.Inject
|
||||
|
||||
class DefaultLogoutUseCase @AssistedInject constructor(
|
||||
@Assisted private val sessionId: String,
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultLogoutUseCase @Inject constructor(
|
||||
private val authenticationService: MatrixAuthenticationService,
|
||||
private val matrixClientProvider: MatrixClientProvider,
|
||||
) : LogoutUseCase {
|
||||
@ContributesBinding(AppScope::class)
|
||||
@AssistedFactory
|
||||
interface Factory : LogoutUseCase.Factory {
|
||||
override fun create(sessionId: String): DefaultLogoutUseCase
|
||||
}
|
||||
|
||||
override suspend fun logout(ignoreSdkError: Boolean): String? {
|
||||
val matrixClient = matrixClientProvider.getOrRestore(SessionId(sessionId)).getOrThrow()
|
||||
val result = matrixClient.logout(ignoreSdkError = ignoreSdkError)
|
||||
return result
|
||||
val currentSession = authenticationService.getLatestSessionId()
|
||||
return if (currentSession != null) {
|
||||
matrixClientProvider.getOrRestore(currentSession)
|
||||
.getOrThrow()
|
||||
.logout(userInitiated = true, ignoreSdkError = true)
|
||||
} else {
|
||||
error("No session to sign out")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -104,7 +104,7 @@ class LogoutPresenter @Inject constructor(
|
|||
ignoreSdkError: Boolean,
|
||||
) = launch {
|
||||
suspend {
|
||||
matrixClient.logout(ignoreSdkError)
|
||||
matrixClient.logout(userInitiated = true, ignoreSdkError)
|
||||
}.runCatchingUpdatingState(logoutAction)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,36 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2024 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
|
||||
*
|
||||
* https://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.logout.impl
|
||||
|
||||
import com.squareup.anvil.annotations.ContributesTo
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import io.element.android.features.logout.api.LogoutUseCase
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import io.element.android.libraries.matrix.api.user.CurrentSessionIdHolder
|
||||
|
||||
@Module
|
||||
@ContributesTo(SessionScope::class)
|
||||
object SessionLogoutModule {
|
||||
@Provides
|
||||
fun provideLogoutUseCase(
|
||||
currentSessionIdHolder: CurrentSessionIdHolder,
|
||||
factory: DefaultLogoutUseCase.Factory,
|
||||
): LogoutUseCase {
|
||||
return factory.create(currentSessionIdHolder.current.value)
|
||||
}
|
||||
}
|
||||
|
|
@ -86,7 +86,7 @@ class DefaultDirectLogoutPresenter @Inject constructor(
|
|||
ignoreSdkError: Boolean,
|
||||
) = launch {
|
||||
suspend {
|
||||
matrixClient.logout(ignoreSdkError)
|
||||
matrixClient.logout(userInitiated = true, ignoreSdkError)
|
||||
}.runCatchingUpdatingState(logoutAction)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -144,7 +144,7 @@ class LogoutPresenterTest {
|
|||
@Test
|
||||
fun `present - logout with error then cancel`() = runTest {
|
||||
val matrixClient = FakeMatrixClient().apply {
|
||||
logoutLambda = { _ ->
|
||||
logoutLambda = { _, _ ->
|
||||
throw A_THROWABLE
|
||||
}
|
||||
}
|
||||
|
|
@ -172,7 +172,7 @@ class LogoutPresenterTest {
|
|||
@Test
|
||||
fun `present - logout with error then force`() = runTest {
|
||||
val matrixClient = FakeMatrixClient().apply {
|
||||
logoutLambda = { ignoreSdkError ->
|
||||
logoutLambda = { ignoreSdkError, _ ->
|
||||
if (!ignoreSdkError) {
|
||||
throw A_THROWABLE
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -125,7 +125,7 @@ class DefaultDirectLogoutPresenterTest {
|
|||
@Test
|
||||
fun `present - logout with error then cancel`() = runTest {
|
||||
val matrixClient = FakeMatrixClient().apply {
|
||||
logoutLambda = { _ ->
|
||||
logoutLambda = { _, _ ->
|
||||
throw A_THROWABLE
|
||||
}
|
||||
}
|
||||
|
|
@ -153,7 +153,7 @@ class DefaultDirectLogoutPresenterTest {
|
|||
@Test
|
||||
fun `present - logout with error then force`() = runTest {
|
||||
val matrixClient = FakeMatrixClient().apply {
|
||||
logoutLambda = { ignoreSdkError ->
|
||||
logoutLambda = { ignoreSdkError, _ ->
|
||||
if (!ignoreSdkError) {
|
||||
throw A_THROWABLE
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -72,6 +72,7 @@ dependencies {
|
|||
implementation(libs.androidx.constraintlayout.compose)
|
||||
implementation(libs.androidx.media3.exoplayer)
|
||||
implementation(libs.androidx.media3.ui)
|
||||
implementation(libs.sigpwned.emoji4j)
|
||||
implementation(libs.vanniktech.blurhash)
|
||||
implementation(libs.telephoto.zoomableimage)
|
||||
implementation(libs.matrix.emojibase.bindings)
|
||||
|
|
|
|||
|
|
@ -18,11 +18,11 @@ package io.element.android.features.messages.impl
|
|||
|
||||
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
|
||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.UniqueId
|
||||
|
||||
sealed interface MessagesEvents {
|
||||
data class HandleAction(val action: TimelineItemAction, val event: TimelineItem.Event) : MessagesEvents
|
||||
data class ToggleReaction(val emoji: String, val eventId: EventId) : MessagesEvents
|
||||
data class ToggleReaction(val emoji: String, val uniqueId: UniqueId) : MessagesEvents
|
||||
data class InviteDialogDismissed(val action: InviteDialogAction) : MessagesEvents
|
||||
data object Dismiss : MessagesEvents
|
||||
}
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
|
|||
import io.element.android.libraries.designsystem.utils.snackbar.collectSnackbarMessageAsState
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.UniqueId
|
||||
import io.element.android.libraries.matrix.api.permalink.PermalinkParser
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoomInfo
|
||||
|
|
@ -192,7 +192,7 @@ class MessagesPresenter @AssistedInject constructor(
|
|||
)
|
||||
}
|
||||
is MessagesEvents.ToggleReaction -> {
|
||||
localCoroutineScope.toggleReaction(event.emoji, event.eventId)
|
||||
localCoroutineScope.toggleReaction(event.emoji, event.uniqueId)
|
||||
}
|
||||
is MessagesEvents.InviteDialogDismissed -> {
|
||||
hasDismissedInviteDialog = true
|
||||
|
|
@ -313,10 +313,10 @@ class MessagesPresenter @AssistedInject constructor(
|
|||
|
||||
private fun CoroutineScope.toggleReaction(
|
||||
emoji: String,
|
||||
eventId: EventId,
|
||||
uniqueId: UniqueId,
|
||||
) = launch(dispatchers.io) {
|
||||
timelineController.invokeOnCurrentTimeline {
|
||||
toggleReaction(emoji, eventId)
|
||||
toggleReaction(emoji, uniqueId)
|
||||
.onFailure { Timber.e(it) }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -175,8 +175,7 @@ fun MessagesView(
|
|||
}
|
||||
|
||||
fun onEmojiReactionClick(emoji: String, event: TimelineItem.Event) {
|
||||
if (event.eventId == null) return
|
||||
state.eventSink(MessagesEvents.ToggleReaction(emoji, event.eventId))
|
||||
state.eventSink(MessagesEvents.ToggleReaction(emoji, event.id))
|
||||
}
|
||||
|
||||
fun onEmojiReactionLongClick(emoji: String, event: TimelineItem.Event) {
|
||||
|
|
@ -248,7 +247,6 @@ fun MessagesView(
|
|||
state = state.actionListState,
|
||||
onSelectAction = ::onActionSelected,
|
||||
onCustomReactionClick = { event ->
|
||||
if (event.eventId == null) return@ActionListView
|
||||
state.customReactionState.eventSink(CustomReactionEvents.ShowCustomReactionSheet(event))
|
||||
},
|
||||
onEmojiReactionClick = ::onEmojiReactionClick,
|
||||
|
|
@ -256,8 +254,8 @@ fun MessagesView(
|
|||
|
||||
CustomReactionBottomSheet(
|
||||
state = state.customReactionState,
|
||||
onSelectEmoji = { eventId, emoji ->
|
||||
state.eventSink(MessagesEvents.ToggleReaction(emoji.unicode, eventId))
|
||||
onSelectEmoji = { uniqueId, emoji ->
|
||||
state.eventSink(MessagesEvents.ToggleReaction(emoji.unicode, uniqueId))
|
||||
}
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -106,7 +106,6 @@ class ActionListPresenter @Inject constructor(
|
|||
isEventPinned = pinnedEventIds.contains(timelineItem.eventId),
|
||||
)
|
||||
val displayEmojiReactions = usersEventPermissions.canSendReaction &&
|
||||
timelineItem.isRemote &&
|
||||
timelineItem.content.canReact()
|
||||
if (actions.isNotEmpty() || displayEmojiReactions) {
|
||||
target.value = ActionListState.Target.Success(
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ import io.element.android.libraries.architecture.Presenter
|
|||
import io.element.android.libraries.core.bool.orFalse
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.UniqueId
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||
import io.element.android.libraries.matrix.api.room.MessageEventType
|
||||
import io.element.android.libraries.matrix.api.room.isDm
|
||||
|
|
@ -95,7 +96,7 @@ class TimelinePresenter @AssistedInject constructor(
|
|||
val userHasPermissionToSendMessage by room.canSendMessageAsState(type = MessageEventType.ROOM_MESSAGE, updateKey = syncUpdateFlow.value)
|
||||
val userHasPermissionToSendReaction by room.canSendMessageAsState(type = MessageEventType.REACTION, updateKey = syncUpdateFlow.value)
|
||||
|
||||
val prevMostRecentItemId = rememberSaveable { mutableStateOf<String?>(null) }
|
||||
val prevMostRecentItemId = rememberSaveable { mutableStateOf<UniqueId?>(null) }
|
||||
|
||||
val newEventState = remember { mutableStateOf(NewEventState.None) }
|
||||
val messageShield: MutableState<MessageShield?> = remember { mutableStateOf(null) }
|
||||
|
|
@ -242,7 +243,7 @@ class TimelinePresenter @AssistedInject constructor(
|
|||
*/
|
||||
private suspend fun computeNewItemState(
|
||||
timelineItems: ImmutableList<TimelineItem>,
|
||||
prevMostRecentItemId: MutableState<String?>,
|
||||
prevMostRecentItemId: MutableState<UniqueId?>,
|
||||
newEventState: MutableState<NewEventState>
|
||||
) = withContext(dispatchers.computation) {
|
||||
// FromMe is prioritized over FromOther, so skip if we already have a FromMe
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
|||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.TransactionId
|
||||
import io.element.android.libraries.matrix.api.core.UniqueId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState
|
||||
|
|
@ -122,7 +123,10 @@ internal fun aTimelineItemList(content: TimelineItemEventContent): ImmutableList
|
|||
}
|
||||
|
||||
fun aTimelineItemDaySeparator(): TimelineItem.Virtual {
|
||||
return TimelineItem.Virtual(UUID.randomUUID().toString(), aTimelineItemDaySeparatorModel("Today"))
|
||||
return TimelineItem.Virtual(
|
||||
id = UniqueId(UUID.randomUUID().toString()),
|
||||
model = aTimelineItemDaySeparatorModel("Today"),
|
||||
)
|
||||
}
|
||||
|
||||
internal fun aTimelineItemEvent(
|
||||
|
|
@ -144,7 +148,7 @@ internal fun aTimelineItemEvent(
|
|||
messageShield: MessageShield? = null,
|
||||
): TimelineItem.Event {
|
||||
return TimelineItem.Event(
|
||||
id = UUID.randomUUID().toString(),
|
||||
id = UniqueId(UUID.randomUUID().toString()),
|
||||
eventId = eventId,
|
||||
transactionId = transactionId,
|
||||
senderId = UserId("@senderId:domain"),
|
||||
|
|
@ -210,7 +214,7 @@ internal fun aTimelineItemReadReceipts(
|
|||
}
|
||||
|
||||
internal fun aGroupedEvents(
|
||||
id: Long = 0,
|
||||
id: UniqueId = UniqueId("0"),
|
||||
withReadReceipts: Boolean = false,
|
||||
): TimelineItem.GroupedEvents {
|
||||
val event1 = aTimelineItemEvent(
|
||||
|
|
@ -231,7 +235,7 @@ internal fun aGroupedEvents(
|
|||
)
|
||||
val events = listOf(event1, event2)
|
||||
return TimelineItem.GroupedEvents(
|
||||
id = id.toString(),
|
||||
id = id,
|
||||
events = events.toImmutableList(),
|
||||
aggregatedReadReceipts = events.flatMap { it.readReceiptState.receipts }.toImmutableList(),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ fun TimelineItemGroupedEventsRow(
|
|||
eventSink: (TimelineEvents.EventFromTimelineItem) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val isExpanded = rememberSaveable(key = timelineItem.identifier()) { mutableStateOf(false) }
|
||||
val isExpanded = rememberSaveable(key = timelineItem.identifier().value) { mutableStateOf(false) }
|
||||
|
||||
fun onExpandGroupClick() {
|
||||
isExpanded.value = !isExpanded.value
|
||||
|
|
|
|||
|
|
@ -25,13 +25,13 @@ import androidx.compose.ui.Modifier
|
|||
import io.element.android.emojibasebindings.Emoji
|
||||
import io.element.android.libraries.designsystem.theme.components.ModalBottomSheet
|
||||
import io.element.android.libraries.designsystem.theme.components.hide
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.UniqueId
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun CustomReactionBottomSheet(
|
||||
state: CustomReactionState,
|
||||
onSelectEmoji: (EventId, Emoji) -> Unit,
|
||||
onSelectEmoji: (UniqueId, Emoji) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val sheetState = rememberModalBottomSheetState()
|
||||
|
|
@ -43,10 +43,10 @@ fun CustomReactionBottomSheet(
|
|||
}
|
||||
|
||||
fun onEmojiSelectedDismiss(emoji: Emoji) {
|
||||
if (target?.event?.eventId == null) return
|
||||
if (target?.event == null) return
|
||||
sheetState.hide(coroutineScope) {
|
||||
state.eventSink(CustomReactionEvents.DismissCustomReactionSheet)
|
||||
onSelectEmoji(target.event.eventId, emoji)
|
||||
onSelectEmoji(target.event.id, emoji)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ import io.element.android.features.messages.impl.timeline.components.layout.Cont
|
|||
import io.element.android.features.messages.impl.timeline.components.layout.ContentAvoidingLayoutData
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContentProvider
|
||||
import io.element.android.features.messages.impl.utils.containsOnlyEmojis
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
|
|
@ -54,9 +55,15 @@ fun TimelineItemTextView(
|
|||
modifier: Modifier = Modifier,
|
||||
onContentLayoutChange: (ContentAvoidingLayoutData) -> Unit = {},
|
||||
) {
|
||||
val emojiOnly = (content.formattedBody == null || content.formattedBody.toString() == content.body) &&
|
||||
content.body.replace(" ", "").containsOnlyEmojis()
|
||||
val textStyle = when {
|
||||
emojiOnly -> ElementTheme.typography.fontHeadingXlRegular
|
||||
else -> ElementTheme.typography.fontBodyLgRegular
|
||||
}
|
||||
CompositionLocalProvider(
|
||||
LocalContentColor provides ElementTheme.colors.textPrimary,
|
||||
LocalTextStyle provides ElementTheme.typography.fontBodyLgRegular
|
||||
LocalTextStyle provides textStyle
|
||||
) {
|
||||
val body = getTextWithResolvedMentions(content)
|
||||
Box(modifier.semantics { contentDescription = content.plainText }) {
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import androidx.annotation.VisibleForTesting
|
|||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||
import io.element.android.libraries.di.RoomScope
|
||||
import io.element.android.libraries.di.SingleIn
|
||||
import io.element.android.libraries.matrix.api.core.UniqueId
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import javax.inject.Inject
|
||||
|
||||
|
|
@ -71,7 +72,7 @@ private fun MutableList<TimelineItem>.addGroup(
|
|||
val groupId = groupIds.getOrPutGroupId(groupOfItems)
|
||||
add(
|
||||
TimelineItem.GroupedEvents(
|
||||
id = groupId,
|
||||
id = UniqueId(groupId),
|
||||
events = groupOfItems.toImmutableList(),
|
||||
aggregatedReadReceipts = groupOfItems.flatMap { it.readReceiptState.receipts }.toImmutableList()
|
||||
)
|
||||
|
|
@ -83,15 +84,15 @@ private fun MutableMap<String, String>.getOrPutGroupId(timelineItems: List<Timel
|
|||
assert(timelineItems.isNotEmpty())
|
||||
for (item in timelineItems) {
|
||||
val itemIdentifier = item.identifier()
|
||||
if (this.contains(itemIdentifier)) {
|
||||
return this[itemIdentifier]!!
|
||||
if (this.contains(itemIdentifier.value)) {
|
||||
return this[itemIdentifier.value]!!
|
||||
}
|
||||
}
|
||||
val timelineItem = timelineItems.first()
|
||||
return computeGroupIdWith(timelineItem).also { groupId ->
|
||||
this[timelineItem.identifier()] = groupId
|
||||
return computeGroupIdWith(timelineItem).value.also { groupId ->
|
||||
this[timelineItem.identifier().value] = groupId
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
internal fun computeGroupIdWith(timelineItem: TimelineItem): String = "${timelineItem.identifier()}_group"
|
||||
internal fun computeGroupIdWith(timelineItem: TimelineItem): UniqueId = UniqueId("${timelineItem.identifier()}_group")
|
||||
|
|
|
|||
|
|
@ -38,10 +38,10 @@ fun anAggregatedReaction(
|
|||
count: Int = 1,
|
||||
isHighlighted: Boolean = false,
|
||||
): AggregatedReaction {
|
||||
val timeFormatter = DateFormat.getTimeInstance(DateFormat.SHORT, java.util.Locale.US)
|
||||
val date = Date(1_689_061_264L)
|
||||
val senders = buildList {
|
||||
repeat(count) { index ->
|
||||
val timeFormatter = DateFormat.getTimeInstance(DateFormat.SHORT)
|
||||
val date = Date(1_689_061_264L)
|
||||
add(
|
||||
AggregatedReactionSender(
|
||||
senderId = if (isHighlighted && index == 0) userId else UserId("@user$index:server.org"),
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import io.element.android.features.messages.impl.timeline.model.virtual.Timeline
|
|||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.TransactionId
|
||||
import io.element.android.libraries.matrix.api.core.UniqueId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState
|
||||
|
|
@ -36,7 +37,7 @@ import kotlinx.collections.immutable.ImmutableList
|
|||
|
||||
@Immutable
|
||||
sealed interface TimelineItem {
|
||||
fun identifier(): String = when (this) {
|
||||
fun identifier(): UniqueId = when (this) {
|
||||
is Event -> id
|
||||
is Virtual -> id
|
||||
is GroupedEvents -> id
|
||||
|
|
@ -58,13 +59,13 @@ sealed interface TimelineItem {
|
|||
|
||||
@Immutable
|
||||
data class Virtual(
|
||||
val id: String,
|
||||
val id: UniqueId,
|
||||
val model: TimelineItemVirtualModel
|
||||
) : TimelineItem
|
||||
|
||||
@Immutable
|
||||
data class Event(
|
||||
val id: String,
|
||||
val id: UniqueId,
|
||||
// Note: eventId can be null when the event is a local echo
|
||||
val eventId: EventId? = null,
|
||||
val transactionId: TransactionId? = null,
|
||||
|
|
@ -101,7 +102,7 @@ sealed interface TimelineItem {
|
|||
|
||||
@Immutable
|
||||
data class GroupedEvents(
|
||||
val id: String,
|
||||
val id: UniqueId,
|
||||
val events: ImmutableList<Event>,
|
||||
val aggregatedReadReceipts: ImmutableList<ReadReceiptData>,
|
||||
) : TimelineItem
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ class TimelineItemEventContentProvider : PreviewParameterProvider<TimelineItemEv
|
|||
aTimelineItemTextContent(),
|
||||
aTimelineItemUnknownContent(),
|
||||
aTimelineItemTextContent().copy(isEdited = true),
|
||||
aTimelineItemTextContent(body = "😁")
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* Copyright (c) 2024 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
|
||||
*
|
||||
* https://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.messages.impl.utils
|
||||
|
||||
import com.sigpwned.emoji4j.core.Grapheme.Type.EMOJI
|
||||
import com.sigpwned.emoji4j.core.Grapheme.Type.PICTOGRAPHIC
|
||||
import com.sigpwned.emoji4j.core.GraphemeMatchResult
|
||||
import com.sigpwned.emoji4j.core.GraphemeMatcher
|
||||
|
||||
/**
|
||||
* Returns true if the string consists exclusively of "emoji or pictographic graphemes".
|
||||
*/
|
||||
fun String.containsOnlyEmojis(): Boolean {
|
||||
if (isEmpty()) return false
|
||||
|
||||
val matcher = GraphemeMatcher(this)
|
||||
var m: GraphemeMatchResult? = null
|
||||
var contiguous = true
|
||||
var previous = 0
|
||||
while (contiguous && matcher.find()) {
|
||||
m = matcher.toMatchResult()
|
||||
// Many non-"emoji" characters are pictographics. We only want to identify this specific range
|
||||
// https://en.wikipedia.org/wiki/Miscellaneous_Symbols_and_Pictographs
|
||||
val isEmoji = m!!.grapheme().type == EMOJI || m.grapheme().type == PICTOGRAPHIC && m.group() in "🌍".."🗺"
|
||||
contiguous = isEmoji and (m.start() == previous)
|
||||
previous = m.end()
|
||||
}
|
||||
|
||||
return contiguous and (m?.end() == length)
|
||||
}
|
||||
|
|
@ -69,6 +69,7 @@ import io.element.android.libraries.featureflag.api.FeatureFlags
|
|||
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.TransactionId
|
||||
import io.element.android.libraries.matrix.api.core.UniqueId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.media.MediaSource
|
||||
import io.element.android.libraries.matrix.api.permalink.PermalinkParser
|
||||
|
|
@ -77,11 +78,11 @@ import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
|
|||
import io.element.android.libraries.matrix.api.room.MessageEventType
|
||||
import io.element.android.libraries.matrix.api.room.RoomMembershipState
|
||||
import io.element.android.libraries.matrix.test.AN_AVATAR_URL
|
||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||
import io.element.android.libraries.matrix.test.A_SESSION_ID_2
|
||||
import io.element.android.libraries.matrix.test.A_THROWABLE
|
||||
import io.element.android.libraries.matrix.test.A_UNIQUE_ID
|
||||
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
||||
import io.element.android.libraries.matrix.test.permalink.FakePermalinkBuilder
|
||||
import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser
|
||||
|
|
@ -197,8 +198,8 @@ class MessagesPresenterTest {
|
|||
@Test
|
||||
fun `present - handle toggling a reaction`() = runTest {
|
||||
val coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true)
|
||||
val toggleReactionSuccess = lambdaRecorder { _: String, _: EventId -> Result.success(Unit) }
|
||||
val toggleReactionFailure = lambdaRecorder { _: String, _: EventId -> Result.failure<Unit>(IllegalStateException("Failed to send reaction")) }
|
||||
val toggleReactionSuccess = lambdaRecorder { _: String, _: UniqueId -> Result.success(Unit) }
|
||||
val toggleReactionFailure = lambdaRecorder { _: String, _: UniqueId -> Result.failure<Unit>(IllegalStateException("Failed to send reaction")) }
|
||||
|
||||
val timeline = FakeTimeline().apply {
|
||||
this.toggleReactionLambda = toggleReactionSuccess
|
||||
|
|
@ -218,25 +219,25 @@ class MessagesPresenterTest {
|
|||
}.test {
|
||||
skipItems(1)
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink.invoke(MessagesEvents.ToggleReaction("👍", AN_EVENT_ID))
|
||||
initialState.eventSink.invoke(MessagesEvents.ToggleReaction("👍", A_UNIQUE_ID))
|
||||
// No crashes when sending a reaction failed
|
||||
timeline.apply { toggleReactionLambda = toggleReactionFailure }
|
||||
initialState.eventSink.invoke(MessagesEvents.ToggleReaction("👍", AN_EVENT_ID))
|
||||
initialState.eventSink.invoke(MessagesEvents.ToggleReaction("👍", A_UNIQUE_ID))
|
||||
assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None)
|
||||
|
||||
assert(toggleReactionSuccess)
|
||||
.isCalledOnce()
|
||||
.with(value("👍"), value(AN_EVENT_ID))
|
||||
.with(value("👍"), value(A_UNIQUE_ID))
|
||||
assert(toggleReactionFailure)
|
||||
.isCalledOnce()
|
||||
.with(value("👍"), value(AN_EVENT_ID))
|
||||
.with(value("👍"), value(A_UNIQUE_ID))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - handle toggling a reaction twice`() = runTest {
|
||||
val coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true)
|
||||
val toggleReactionSuccess = lambdaRecorder { _: String, _: EventId -> Result.success(Unit) }
|
||||
val toggleReactionSuccess = lambdaRecorder { _: String, _: UniqueId -> Result.success(Unit) }
|
||||
|
||||
val timeline = FakeTimeline().apply {
|
||||
this.toggleReactionLambda = toggleReactionSuccess
|
||||
|
|
@ -255,13 +256,13 @@ class MessagesPresenterTest {
|
|||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitFirstItem()
|
||||
initialState.eventSink.invoke(MessagesEvents.ToggleReaction("👍", AN_EVENT_ID))
|
||||
initialState.eventSink.invoke(MessagesEvents.ToggleReaction("👍", AN_EVENT_ID))
|
||||
initialState.eventSink.invoke(MessagesEvents.ToggleReaction("👍", A_UNIQUE_ID))
|
||||
initialState.eventSink.invoke(MessagesEvents.ToggleReaction("👍", A_UNIQUE_ID))
|
||||
assert(toggleReactionSuccess)
|
||||
.isCalledExactly(2)
|
||||
.withSequence(
|
||||
listOf(value("👍"), value(AN_EVENT_ID)),
|
||||
listOf(value("👍"), value(AN_EVENT_ID)),
|
||||
listOf(value("👍"), value(A_UNIQUE_ID)),
|
||||
listOf(value("👍"), value(A_UNIQUE_ID)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -361,7 +361,7 @@ class MessagesViewTest {
|
|||
state = state,
|
||||
)
|
||||
rule.onAllNodesWithText("👍️").onFirst().performClick()
|
||||
eventsRecorder.assertSingle(MessagesEvents.ToggleReaction("👍️", timelineItem.eventId!!))
|
||||
eventsRecorder.assertSingle(MessagesEvents.ToggleReaction("👍️", timelineItem.id))
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -463,7 +463,7 @@ class MessagesViewTest {
|
|||
// Give time for the close animation to complete
|
||||
rule.mainClock.advanceTimeBy(milliseconds = 1_000)
|
||||
customReactionStateEventsRecorder.assertSingle(CustomReactionEvents.DismissCustomReactionSheet)
|
||||
eventsRecorder.assertSingle(MessagesEvents.ToggleReaction(aUnicode, timelineItem.eventId!!))
|
||||
eventsRecorder.assertSingle(MessagesEvents.ToggleReaction(aUnicode, timelineItem.id))
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
|||
|
|
@ -757,7 +757,7 @@ class ActionListPresenterTest {
|
|||
assertThat(successState.target).isEqualTo(
|
||||
ActionListState.Target.Success(
|
||||
event = messageEvent,
|
||||
displayEmojiReactions = false,
|
||||
displayEmojiReactions = true,
|
||||
actions = persistentListOf(
|
||||
TimelineItemAction.Edit,
|
||||
TimelineItemAction.Copy,
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
|||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.TransactionId
|
||||
import io.element.android.libraries.matrix.api.core.UniqueId
|
||||
import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.MessageShield
|
||||
|
|
@ -51,7 +52,7 @@ internal fun aMessageEvent(
|
|||
sendState: LocalEventSendState = LocalEventSendState.Sent(AN_EVENT_ID),
|
||||
messageShield: MessageShield? = null,
|
||||
) = TimelineItem.Event(
|
||||
id = eventId?.value.orEmpty(),
|
||||
id = UniqueId(eventId?.value.orEmpty()),
|
||||
eventId = eventId,
|
||||
transactionId = transactionId,
|
||||
senderId = A_USER_ID,
|
||||
|
|
|
|||
|
|
@ -24,6 +24,8 @@ import io.element.android.libraries.matrix.api.room.MatrixRoom
|
|||
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
|
||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID_2
|
||||
import io.element.android.libraries.matrix.test.A_UNIQUE_ID
|
||||
import io.element.android.libraries.matrix.test.A_UNIQUE_ID_2
|
||||
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
||||
import io.element.android.libraries.matrix.test.room.aRoomInfo
|
||||
import io.element.android.libraries.matrix.test.timeline.FakeTimeline
|
||||
|
|
@ -80,7 +82,7 @@ class PinnedMessagesBannerPresenterTest {
|
|||
timelineItems = flowOf(
|
||||
listOf(
|
||||
MatrixTimelineItem.Event(
|
||||
uniqueId = "FAKE_UNIQUE_ID",
|
||||
uniqueId = A_UNIQUE_ID,
|
||||
event = anEventTimelineItem(
|
||||
eventId = AN_EVENT_ID,
|
||||
content = messageContent,
|
||||
|
|
@ -112,14 +114,14 @@ class PinnedMessagesBannerPresenterTest {
|
|||
timelineItems = flowOf(
|
||||
listOf(
|
||||
MatrixTimelineItem.Event(
|
||||
uniqueId = "FAKE_UNIQUE_ID",
|
||||
uniqueId = A_UNIQUE_ID,
|
||||
event = anEventTimelineItem(
|
||||
eventId = AN_EVENT_ID,
|
||||
content = messageContent1,
|
||||
),
|
||||
),
|
||||
MatrixTimelineItem.Event(
|
||||
uniqueId = "FAKE_UNIQUE_ID_2",
|
||||
uniqueId = A_UNIQUE_ID_2,
|
||||
event = anEventTimelineItem(
|
||||
eventId = AN_EVENT_ID_2,
|
||||
content = messageContent2,
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import com.google.common.truth.Truth.assertThat
|
|||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemReadMarkerModel
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.UniqueId
|
||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||
import org.junit.Test
|
||||
|
||||
|
|
@ -34,7 +35,7 @@ class TimelineItemIndexerTest {
|
|||
groupedEvents.events.forEach { eventIds.add(it.eventId!!) }
|
||||
},
|
||||
TimelineItem.Virtual(
|
||||
id = "dummy",
|
||||
id = UniqueId("dummy"),
|
||||
model = TimelineItemReadMarkerModel
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ import io.element.android.features.poll.api.actions.SendPollResponseAction
|
|||
import io.element.android.features.poll.test.actions.FakeEndPollAction
|
||||
import io.element.android.features.poll.test.actions.FakeSendPollResponseAction
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.UniqueId
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
|
||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
|
||||
import io.element.android.libraries.matrix.api.timeline.ReceiptType
|
||||
|
|
@ -46,6 +47,8 @@ import io.element.android.libraries.matrix.api.timeline.item.event.Receipt
|
|||
import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem
|
||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID_2
|
||||
import io.element.android.libraries.matrix.test.A_UNIQUE_ID
|
||||
import io.element.android.libraries.matrix.test.A_UNIQUE_ID_2
|
||||
import io.element.android.libraries.matrix.test.A_USER_ID
|
||||
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
||||
import io.element.android.libraries.matrix.test.room.aRoomMember
|
||||
|
|
@ -79,9 +82,6 @@ import java.util.Date
|
|||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
private const val FAKE_UNIQUE_ID = "FAKE_UNIQUE_ID"
|
||||
private const val FAKE_UNIQUE_ID_2 = "FAKE_UNIQUE_ID_2"
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class) class TimelinePresenterTest {
|
||||
@get:Rule
|
||||
val warmUpRule = WarmUpRule()
|
||||
|
|
@ -131,7 +131,7 @@ private const val FAKE_UNIQUE_ID_2 = "FAKE_UNIQUE_ID_2"
|
|||
val timeline = FakeTimeline(
|
||||
timelineItems = flowOf(
|
||||
listOf(
|
||||
MatrixTimelineItem.Event(FAKE_UNIQUE_ID, anEventTimelineItem())
|
||||
MatrixTimelineItem.Event(A_UNIQUE_ID, anEventTimelineItem())
|
||||
)
|
||||
)
|
||||
)
|
||||
|
|
@ -164,9 +164,9 @@ private const val FAKE_UNIQUE_ID_2 = "FAKE_UNIQUE_ID_2"
|
|||
val timeline = FakeTimeline(
|
||||
timelineItems = flowOf(
|
||||
listOf(
|
||||
MatrixTimelineItem.Event(FAKE_UNIQUE_ID, anEventTimelineItem()),
|
||||
MatrixTimelineItem.Event(A_UNIQUE_ID, anEventTimelineItem()),
|
||||
MatrixTimelineItem.Event(
|
||||
uniqueId = FAKE_UNIQUE_ID_2,
|
||||
uniqueId = A_UNIQUE_ID_2,
|
||||
event = anEventTimelineItem(
|
||||
eventId = AN_EVENT_ID_2,
|
||||
content = aMessageContent("Test message")
|
||||
|
|
@ -201,9 +201,9 @@ private const val FAKE_UNIQUE_ID_2 = "FAKE_UNIQUE_ID_2"
|
|||
val timeline = FakeTimeline(
|
||||
timelineItems = flowOf(
|
||||
listOf(
|
||||
MatrixTimelineItem.Event(FAKE_UNIQUE_ID, anEventTimelineItem()),
|
||||
MatrixTimelineItem.Event(A_UNIQUE_ID, anEventTimelineItem()),
|
||||
MatrixTimelineItem.Event(
|
||||
uniqueId = FAKE_UNIQUE_ID_2,
|
||||
uniqueId = A_UNIQUE_ID_2,
|
||||
event = anEventTimelineItem(
|
||||
eventId = AN_EVENT_ID_2,
|
||||
content = aMessageContent("Test message")
|
||||
|
|
@ -243,9 +243,9 @@ private const val FAKE_UNIQUE_ID_2 = "FAKE_UNIQUE_ID_2"
|
|||
val timeline = FakeTimeline(
|
||||
timelineItems = flowOf(
|
||||
listOf(
|
||||
MatrixTimelineItem.Event(FAKE_UNIQUE_ID, anEventTimelineItem()),
|
||||
MatrixTimelineItem.Event(A_UNIQUE_ID, anEventTimelineItem()),
|
||||
MatrixTimelineItem.Event(
|
||||
uniqueId = FAKE_UNIQUE_ID_2,
|
||||
uniqueId = A_UNIQUE_ID_2,
|
||||
event = anEventTimelineItem(
|
||||
eventId = AN_EVENT_ID_2,
|
||||
content = aMessageContent("Test message")
|
||||
|
|
@ -279,8 +279,8 @@ private const val FAKE_UNIQUE_ID_2 = "FAKE_UNIQUE_ID_2"
|
|||
val timeline = FakeTimeline(
|
||||
timelineItems = flowOf(
|
||||
listOf(
|
||||
MatrixTimelineItem.Virtual(FAKE_UNIQUE_ID, VirtualTimelineItem.ReadMarker),
|
||||
MatrixTimelineItem.Virtual(FAKE_UNIQUE_ID, VirtualTimelineItem.ReadMarker)
|
||||
MatrixTimelineItem.Virtual(A_UNIQUE_ID, VirtualTimelineItem.ReadMarker),
|
||||
MatrixTimelineItem.Virtual(A_UNIQUE_ID, VirtualTimelineItem.ReadMarker)
|
||||
)
|
||||
)
|
||||
).apply {
|
||||
|
|
@ -310,13 +310,13 @@ private const val FAKE_UNIQUE_ID_2 = "FAKE_UNIQUE_ID_2"
|
|||
assertThat(initialState.newEventState).isEqualTo(NewEventState.None)
|
||||
assertThat(initialState.timelineItems.size).isEqualTo(0)
|
||||
timelineItems.emit(
|
||||
listOf(MatrixTimelineItem.Event("0", anEventTimelineItem(content = aMessageContent())))
|
||||
listOf(MatrixTimelineItem.Event(UniqueId("0"), anEventTimelineItem(content = aMessageContent())))
|
||||
)
|
||||
consumeItemsUntilPredicate { it.timelineItems.size == 1 }
|
||||
// Mimics sending a message, and assert newEventState is FromMe
|
||||
timelineItems.getAndUpdate { items ->
|
||||
val event = anEventTimelineItem(content = aMessageContent(), isOwn = true)
|
||||
items + listOf(MatrixTimelineItem.Event("1", event))
|
||||
items + listOf(MatrixTimelineItem.Event(UniqueId("1"), event))
|
||||
}
|
||||
consumeItemsUntilPredicate { it.timelineItems.size == 2 }
|
||||
awaitLastSequentialItem().also { state ->
|
||||
|
|
@ -325,7 +325,7 @@ private const val FAKE_UNIQUE_ID_2 = "FAKE_UNIQUE_ID_2"
|
|||
// Mimics receiving a message without clearing the previous FromMe
|
||||
timelineItems.getAndUpdate { items ->
|
||||
val event = anEventTimelineItem(content = aMessageContent())
|
||||
items + listOf(MatrixTimelineItem.Event("2", event))
|
||||
items + listOf(MatrixTimelineItem.Event(UniqueId("2"), event))
|
||||
}
|
||||
consumeItemsUntilPredicate { it.timelineItems.size == 3 }
|
||||
|
||||
|
|
@ -337,7 +337,7 @@ private const val FAKE_UNIQUE_ID_2 = "FAKE_UNIQUE_ID_2"
|
|||
// Mimics receiving a message and assert newEventState is FromOther
|
||||
timelineItems.getAndUpdate { items ->
|
||||
val event = anEventTimelineItem(content = aMessageContent())
|
||||
items + listOf(MatrixTimelineItem.Event("3", event))
|
||||
items + listOf(MatrixTimelineItem.Event(UniqueId("3"), event))
|
||||
}
|
||||
consumeItemsUntilPredicate { it.timelineItems.size == 4 }
|
||||
awaitLastSequentialItem().also { state ->
|
||||
|
|
@ -381,7 +381,7 @@ private const val FAKE_UNIQUE_ID_2 = "FAKE_UNIQUE_ID_2"
|
|||
),
|
||||
)
|
||||
timelineItems.emit(
|
||||
listOf(MatrixTimelineItem.Event(FAKE_UNIQUE_ID, anEventTimelineItem(reactions = oneReaction)))
|
||||
listOf(MatrixTimelineItem.Event(A_UNIQUE_ID, anEventTimelineItem(reactions = oneReaction)))
|
||||
)
|
||||
val item = awaitItem().timelineItems.first()
|
||||
assertThat(item).isInstanceOf(TimelineItem.Event::class.java)
|
||||
|
|
@ -476,7 +476,7 @@ private const val FAKE_UNIQUE_ID_2 = "FAKE_UNIQUE_ID_2"
|
|||
timelineItems = flowOf(
|
||||
listOf(
|
||||
MatrixTimelineItem.Event(
|
||||
uniqueId = FAKE_UNIQUE_ID,
|
||||
uniqueId = A_UNIQUE_ID,
|
||||
event = anEventTimelineItem(),
|
||||
)
|
||||
)
|
||||
|
|
@ -532,7 +532,7 @@ private const val FAKE_UNIQUE_ID_2 = "FAKE_UNIQUE_ID_2"
|
|||
timelineItems = flowOf(
|
||||
listOf(
|
||||
MatrixTimelineItem.Event(
|
||||
uniqueId = FAKE_UNIQUE_ID,
|
||||
uniqueId = A_UNIQUE_ID,
|
||||
event = anEventTimelineItem(eventId = AN_EVENT_ID),
|
||||
)
|
||||
)
|
||||
|
|
@ -618,7 +618,7 @@ private const val FAKE_UNIQUE_ID_2 = "FAKE_UNIQUE_ID_2"
|
|||
timelineItems = flowOf(
|
||||
listOf(
|
||||
MatrixTimelineItem.Event(
|
||||
FAKE_UNIQUE_ID,
|
||||
A_UNIQUE_ID,
|
||||
anEventTimelineItem(
|
||||
sender = A_USER_ID,
|
||||
receipts = persistentListOf(
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ import io.element.android.features.messages.impl.timeline.model.event.aTimelineI
|
|||
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemLoadingIndicatorModel
|
||||
import io.element.android.features.messages.impl.typing.TypingNotificationState
|
||||
import io.element.android.features.messages.impl.typing.aTypingNotificationState
|
||||
import io.element.android.libraries.matrix.api.core.UniqueId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.timeline.Timeline
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.MessageShield
|
||||
|
|
@ -54,7 +55,7 @@ class TimelineViewTest {
|
|||
state = aTimelineState(
|
||||
timelineItems = persistentListOf<TimelineItem>(
|
||||
TimelineItem.Virtual(
|
||||
id = "backward_pagination",
|
||||
id = UniqueId("backward_pagination"),
|
||||
model = TimelineItemLoadingIndicatorModel(Timeline.PaginationDirection.BACKWARDS, 0)
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import io.element.android.features.messages.impl.timeline.model.TimelineItemRead
|
|||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateEventContent
|
||||
import io.element.android.features.messages.impl.timeline.model.virtual.aTimelineItemDaySeparatorModel
|
||||
import io.element.android.libraries.designsystem.components.avatar.anAvatarData
|
||||
import io.element.android.libraries.matrix.api.core.UniqueId
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState
|
||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||
import io.element.android.libraries.matrix.test.A_USER_ID
|
||||
|
|
@ -37,7 +38,7 @@ class TimelineItemGrouperTest {
|
|||
private val sut = TimelineItemGrouper()
|
||||
|
||||
private val aGroupableItem = TimelineItem.Event(
|
||||
id = "0",
|
||||
id = UniqueId("0"),
|
||||
senderId = A_USER_ID,
|
||||
senderAvatar = anAvatarData(),
|
||||
senderProfile = aProfileTimelineDetailsReady(displayName = ""),
|
||||
|
|
@ -54,7 +55,7 @@ class TimelineItemGrouperTest {
|
|||
messageShield = null,
|
||||
)
|
||||
private val aNonGroupableItem = aMessageEvent()
|
||||
private val aNonGroupableItemNoEvent = TimelineItem.Virtual("virtual", aTimelineItemDaySeparatorModel("Today"))
|
||||
private val aNonGroupableItemNoEvent = TimelineItem.Virtual(UniqueId("virtual"), aTimelineItemDaySeparatorModel("Today"))
|
||||
|
||||
@Test
|
||||
fun `test empty`() {
|
||||
|
|
@ -82,8 +83,8 @@ class TimelineItemGrouperTest {
|
|||
fun `test groupables and ensure reordering`() {
|
||||
val result = sut.group(
|
||||
listOf(
|
||||
aGroupableItem.copy(id = "1"),
|
||||
aGroupableItem.copy(id = "0"),
|
||||
aGroupableItem.copy(id = UniqueId("1")),
|
||||
aGroupableItem.copy(id = UniqueId("0")),
|
||||
),
|
||||
)
|
||||
assertThat(result).isEqualTo(
|
||||
|
|
@ -91,8 +92,8 @@ class TimelineItemGrouperTest {
|
|||
TimelineItem.GroupedEvents(
|
||||
id = computeGroupIdWith(aGroupableItem),
|
||||
events = listOf(
|
||||
aGroupableItem.copy("0"),
|
||||
aGroupableItem.copy(id = "1"),
|
||||
aGroupableItem.copy(id = UniqueId("0")),
|
||||
aGroupableItem.copy(id = UniqueId("1")),
|
||||
).toImmutableList(),
|
||||
aggregatedReadReceipts = emptyList<ReadReceiptData>().toImmutableList(),
|
||||
),
|
||||
|
|
@ -161,13 +162,13 @@ class TimelineItemGrouperTest {
|
|||
fun `when calling multiple time the method group over a growing list of groupable items, then groupId is stable`() {
|
||||
// When
|
||||
val groupableItems = mutableListOf(
|
||||
aGroupableItem.copy(id = "1"),
|
||||
aGroupableItem.copy(id = "2")
|
||||
aGroupableItem.copy(id = UniqueId("1")),
|
||||
aGroupableItem.copy(id = UniqueId("2"))
|
||||
)
|
||||
val expectedGroupId = sut.group(groupableItems).first().identifier()
|
||||
groupableItems.add(0, aGroupableItem.copy("3"))
|
||||
groupableItems.add(2, aGroupableItem.copy("4"))
|
||||
groupableItems.add(aGroupableItem.copy("5"))
|
||||
groupableItems.add(0, aGroupableItem.copy(UniqueId("3")))
|
||||
groupableItems.add(2, aGroupableItem.copy(UniqueId("4")))
|
||||
groupableItems.add(aGroupableItem.copy(UniqueId("5")))
|
||||
val actualGroupId = sut.group(groupableItems).first().identifier()
|
||||
// Then
|
||||
assertThat(actualGroupId).isEqualTo(expectedGroupId)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* Copyright (c) 2024 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
|
||||
*
|
||||
* https://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.messages.impl.utils
|
||||
|
||||
import org.junit.Assert
|
||||
import org.junit.Test
|
||||
|
||||
class EmojiTest {
|
||||
@Test
|
||||
fun validEmojis() {
|
||||
// Simple single/multiple single-codepoint emojis per string
|
||||
Assert.assertTrue("👍".containsOnlyEmojis())
|
||||
Assert.assertTrue("😀".containsOnlyEmojis())
|
||||
Assert.assertTrue("🙂🙁".containsOnlyEmojis())
|
||||
Assert.assertTrue("👁❤️🍝".containsOnlyEmojis()) // 👁 is a pictographic
|
||||
Assert.assertTrue("👨👩👦1️⃣🚀👳🏾♂️🪩".containsOnlyEmojis())
|
||||
Assert.assertTrue("🌍🌎🌏".containsOnlyEmojis())
|
||||
|
||||
// Awkward multi-codepoint graphemes
|
||||
Assert.assertTrue("🧑🧑🧒🧒".containsOnlyEmojis())
|
||||
Assert.assertTrue("🏴☠".containsOnlyEmojis())
|
||||
Assert.assertTrue("👩🏿🔧".containsOnlyEmojis())
|
||||
|
||||
Assert.assertFalse("".containsOnlyEmojis())
|
||||
Assert.assertFalse(" ".containsOnlyEmojis())
|
||||
Assert.assertFalse("🙂 🙁".containsOnlyEmojis())
|
||||
Assert.assertFalse(" 🙂 🙁 ".containsOnlyEmojis())
|
||||
Assert.assertFalse("Hello".containsOnlyEmojis())
|
||||
Assert.assertFalse("Hello 👋".containsOnlyEmojis())
|
||||
}
|
||||
}
|
||||
|
|
@ -19,6 +19,7 @@ package io.element.android.features.messages.impl.voicemessages.timeline
|
|||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.core.mimetype.MimeTypes
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.UniqueId
|
||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
|
||||
import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem
|
||||
|
|
@ -80,7 +81,7 @@ fun TestScope.aDefaultRedactedVoiceMessageManager(
|
|||
|
||||
fun aRedactedMatrixTimeline(eventId: EventId) = listOf<MatrixTimelineItem>(
|
||||
MatrixTimelineItem.Event(
|
||||
uniqueId = "0",
|
||||
uniqueId = UniqueId("0"),
|
||||
event = EventTimelineItem(
|
||||
eventId = eventId,
|
||||
transactionId = null,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* Copyright (c) 2024 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.migration.impl.migrations
|
||||
|
||||
import com.squareup.anvil.annotations.ContributesMultibinding
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.di.CacheDirectory
|
||||
import io.element.android.libraries.sessionstorage.api.SessionStore
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Create the cache directory for the existing sessions.
|
||||
*/
|
||||
@ContributesMultibinding(AppScope::class)
|
||||
class AppMigration06 @Inject constructor(
|
||||
private val sessionStore: SessionStore,
|
||||
@CacheDirectory private val cacheDirectory: File,
|
||||
) : AppMigration {
|
||||
override val order: Int = 6
|
||||
|
||||
override suspend fun migrate() {
|
||||
val allSessions = sessionStore.getAllSessions()
|
||||
for (session in allSessions) {
|
||||
if (session.cachePath.isEmpty()) {
|
||||
val sessionFile = File(session.sessionPath)
|
||||
val sessionFolder = sessionFile.name
|
||||
val cachePath = File(cacheDirectory, sessionFolder)
|
||||
sessionStore.updateData(session.copy(cachePath = cachePath.absolutePath))
|
||||
// Move existing cache files
|
||||
listOf(
|
||||
"matrix-sdk-event-cache.sqlite3",
|
||||
"matrix-sdk-event-cache.sqlite3-shm",
|
||||
"matrix-sdk-event-cache.sqlite3-wal",
|
||||
).map { fileName ->
|
||||
File(sessionFile, fileName)
|
||||
}.takeIf { files ->
|
||||
files.all { it.exists() }
|
||||
}?.forEach { cacheFile ->
|
||||
val targetFile = File(cachePath, cacheFile.name)
|
||||
cacheFile.copyTo(targetFile)
|
||||
cacheFile.delete()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -17,6 +17,7 @@
|
|||
package io.element.android.features.poll.impl
|
||||
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.UniqueId
|
||||
import io.element.android.libraries.matrix.api.poll.PollAnswer
|
||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.PollContent
|
||||
|
|
@ -32,8 +33,8 @@ fun aPollTimelineItems(
|
|||
return flowOf(
|
||||
polls.map { entry ->
|
||||
MatrixTimelineItem.Event(
|
||||
entry.key.value,
|
||||
anEventTimelineItem(
|
||||
uniqueId = UniqueId(entry.key.value),
|
||||
event = anEventTimelineItem(
|
||||
eventId = entry.key,
|
||||
content = entry.value,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -62,6 +62,7 @@ dependencies {
|
|||
implementation(projects.features.lockscreen.api)
|
||||
implementation(projects.features.analytics.api)
|
||||
implementation(projects.features.ftue.api)
|
||||
implementation(projects.features.licenses.api)
|
||||
implementation(projects.features.logout.api)
|
||||
implementation(projects.features.roomlist.api)
|
||||
implementation(projects.services.analytics.api)
|
||||
|
|
@ -91,7 +92,6 @@ dependencies {
|
|||
testImplementation(projects.features.rageshake.test)
|
||||
testImplementation(projects.features.rageshake.impl)
|
||||
testImplementation(projects.features.logout.test)
|
||||
testImplementation(projects.features.roomlist.test)
|
||||
testImplementation(projects.libraries.indicator.impl)
|
||||
testImplementation(projects.libraries.pushproviders.test)
|
||||
testImplementation(projects.libraries.fullscreenintent.test)
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ import com.bumble.appyx.navmodel.backstack.operation.push
|
|||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import io.element.android.anvilannotations.ContributesNode
|
||||
import io.element.android.features.licenses.api.OpenSourceLicensesEntryPoint
|
||||
import io.element.android.features.lockscreen.api.LockScreenEntryPoint
|
||||
import io.element.android.features.logout.api.LogoutEntryPoint
|
||||
import io.element.android.features.preferences.api.PreferencesEntryPoint
|
||||
|
|
@ -59,6 +60,7 @@ class PreferencesFlowNode @AssistedInject constructor(
|
|||
private val lockScreenEntryPoint: LockScreenEntryPoint,
|
||||
private val notificationTroubleShootEntryPoint: NotificationTroubleShootEntryPoint,
|
||||
private val logoutEntryPoint: LogoutEntryPoint,
|
||||
private val openSourceLicensesEntryPoint: OpenSourceLicensesEntryPoint,
|
||||
) : BaseFlowNode<PreferencesFlowNode.NavTarget>(
|
||||
backstack = BackStack(
|
||||
initialElement = plugins.filterIsInstance<PreferencesEntryPoint.Params>().first().initialElement.toNavTarget(),
|
||||
|
|
@ -106,6 +108,9 @@ class PreferencesFlowNode @AssistedInject constructor(
|
|||
|
||||
@Parcelize
|
||||
data object SignOut : NavTarget
|
||||
|
||||
@Parcelize
|
||||
data object OssLicenses : NavTarget
|
||||
}
|
||||
|
||||
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||
|
|
@ -170,7 +175,12 @@ class PreferencesFlowNode @AssistedInject constructor(
|
|||
createNode<ConfigureTracingNode>(buildContext)
|
||||
}
|
||||
NavTarget.About -> {
|
||||
createNode<AboutNode>(buildContext)
|
||||
val callback = object : AboutNode.Callback {
|
||||
override fun openOssLicenses() {
|
||||
backstack.push(NavTarget.OssLicenses)
|
||||
}
|
||||
}
|
||||
createNode<AboutNode>(buildContext, listOf(callback))
|
||||
}
|
||||
NavTarget.AnalyticsSettings -> {
|
||||
createNode<AnalyticsSettingsNode>(buildContext)
|
||||
|
|
@ -232,6 +242,9 @@ class PreferencesFlowNode @AssistedInject constructor(
|
|||
.callback(callBack)
|
||||
.build()
|
||||
}
|
||||
is NavTarget.OssLicenses -> {
|
||||
openSourceLicensesEntryPoint.getNode(this, buildContext)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -27,7 +27,6 @@ import dagger.assisted.Assisted
|
|||
import dagger.assisted.AssistedInject
|
||||
import io.element.android.anvilannotations.ContributesNode
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.features.preferences.api.OpenSourceLicensesProvider
|
||||
import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
|
||||
|
|
@ -36,8 +35,11 @@ class AboutNode @AssistedInject constructor(
|
|||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
private val presenter: AboutPresenter,
|
||||
private val openSourceLicensesProvider: OpenSourceLicensesProvider,
|
||||
) : Node(buildContext, plugins = plugins) {
|
||||
interface Callback : Plugin {
|
||||
fun openOssLicenses()
|
||||
}
|
||||
|
||||
private fun onElementLegalClick(
|
||||
activity: Activity,
|
||||
darkTheme: Boolean,
|
||||
|
|
@ -58,7 +60,7 @@ class AboutNode @AssistedInject constructor(
|
|||
onElementLegalClick(activity, isDark, elementLegal)
|
||||
},
|
||||
onOpenSourceLicensesClick = {
|
||||
openSourceLicensesProvider.navigateToOpenSourceLicenses(activity)
|
||||
plugins.filterIsInstance<Callback>().forEach { it.openOssLicenses() }
|
||||
},
|
||||
modifier = modifier
|
||||
)
|
||||
|
|
|
|||
|
|
@ -17,18 +17,14 @@
|
|||
package io.element.android.features.preferences.impl.about
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import io.element.android.features.preferences.api.OpenSourceLicensesProvider
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import javax.inject.Inject
|
||||
|
||||
class AboutPresenter @Inject constructor(
|
||||
private val openSourceLicensesProvider: OpenSourceLicensesProvider,
|
||||
) : Presenter<AboutState> {
|
||||
class AboutPresenter @Inject constructor() : Presenter<AboutState> {
|
||||
@Composable
|
||||
override fun present(): AboutState {
|
||||
return AboutState(
|
||||
elementLegals = getAllLegals(),
|
||||
hasOpenSourcesLicenses = openSourceLicensesProvider.hasOpenSourceLicenses,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,5 +18,4 @@ package io.element.android.features.preferences.impl.about
|
|||
|
||||
data class AboutState(
|
||||
val elementLegals: List<ElementLegal>,
|
||||
val hasOpenSourcesLicenses: Boolean,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -22,13 +22,11 @@ open class AboutStateProvider : PreviewParameterProvider<AboutState> {
|
|||
override val values: Sequence<AboutState>
|
||||
get() = sequenceOf(
|
||||
anAboutState(),
|
||||
anAboutState(hasOpenSourcesLicenses = true),
|
||||
)
|
||||
}
|
||||
|
||||
fun anAboutState(
|
||||
hasOpenSourcesLicenses: Boolean = false,
|
||||
elementLegals: List<ElementLegal> = getAllLegals(),
|
||||
) = AboutState(
|
||||
elementLegals = getAllLegals(),
|
||||
hasOpenSourcesLicenses = hasOpenSourcesLicenses,
|
||||
elementLegals = elementLegals,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -45,12 +45,10 @@ fun AboutView(
|
|||
onClick = { onElementLegalClick(elementLegal) }
|
||||
)
|
||||
}
|
||||
if (state.hasOpenSourcesLicenses) {
|
||||
PreferenceText(
|
||||
title = stringResource(id = CommonStrings.common_open_source_licenses),
|
||||
onClick = onOpenSourceLicensesClick,
|
||||
)
|
||||
}
|
||||
PreferenceText(
|
||||
title = stringResource(id = CommonStrings.common_open_source_licenses),
|
||||
onClick = onOpenSourceLicensesClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -24,7 +24,6 @@ import coil.annotation.ExperimentalCoilApi
|
|||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.features.ftue.api.state.FtueService
|
||||
import io.element.android.features.preferences.impl.DefaultCacheService
|
||||
import io.element.android.features.roomlist.api.migration.MigrationScreenStore
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
import io.element.android.libraries.di.ApplicationContext
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
|
|
@ -47,7 +46,6 @@ class DefaultClearCacheUseCase @Inject constructor(
|
|||
private val defaultCacheService: DefaultCacheService,
|
||||
private val okHttpClient: Provider<OkHttpClient>,
|
||||
private val ftueService: FtueService,
|
||||
private val migrationScreenStore: MigrationScreenStore,
|
||||
private val pushService: PushService,
|
||||
) : ClearCacheUseCase {
|
||||
override suspend fun invoke() = withContext(coroutineDispatchers.io) {
|
||||
|
|
@ -64,8 +62,6 @@ class DefaultClearCacheUseCase @Inject constructor(
|
|||
context.cacheDir.deleteRecursively()
|
||||
// Clear some settings
|
||||
ftueService.reset()
|
||||
// Clear migration screen store
|
||||
migrationScreenStore.reset()
|
||||
// Ensure any error will be displayed again
|
||||
pushService.setIgnoreRegistrationError(matrixClient.sessionId, false)
|
||||
// Ensure the app is restarted
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ Wenn du fortfährst, können sich einige deiner Einstellungen ändern."</string>
|
|||
<string name="screen_notification_settings_edit_failed_updating_default_mode">"Beim Aktualisieren der Benachrichtigungseinstellungen ist ein Fehler aufgetreten."</string>
|
||||
<string name="screen_notification_settings_edit_mode_all_messages">"Alle Nachrichten"</string>
|
||||
<string name="screen_notification_settings_edit_mode_mentions_and_keywords">"Nur Erwähnungen und Schlüsselwörter"</string>
|
||||
<string name="screen_notification_settings_edit_screen_direct_section_header">"Benachrichtige mich bei Direktnachrichten über "</string>
|
||||
<string name="screen_notification_settings_edit_screen_direct_section_header">"Benachrichtige mich bei Direktnachrichten über"</string>
|
||||
<string name="screen_notification_settings_edit_screen_group_section_header">"Bei Gruppenchats benachrichtige mich bei"</string>
|
||||
<string name="screen_notification_settings_enable_notifications">"Benachrichtigungen auf diesem Gerät aktivieren"</string>
|
||||
<string name="screen_notification_settings_failed_fixing_configuration">"Die Konfiguration wurde nicht korrigiert, bitte versuche es erneut."</string>
|
||||
|
|
|
|||
|
|
@ -31,25 +31,12 @@ class AboutPresenterTest {
|
|||
|
||||
@Test
|
||||
fun `present - initial state`() = runTest {
|
||||
val presenter = AboutPresenter(FakeOpenSourceLicensesProvider(hasOpenSourceLicenses = true))
|
||||
val presenter = AboutPresenter()
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.elementLegals).isEqualTo(getAllLegals())
|
||||
assertThat(initialState.hasOpenSourcesLicenses).isTrue()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - initial state, no open source licenses`() = runTest {
|
||||
val presenter = AboutPresenter(FakeOpenSourceLicensesProvider(hasOpenSourceLicenses = false))
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.elementLegals).isEqualTo(getAllLegals())
|
||||
assertThat(initialState.hasOpenSourcesLicenses).isFalse()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ package io.element.android.features.preferences.impl.about
|
|||
import androidx.activity.ComponentActivity
|
||||
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
|
||||
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import io.element.android.tests.testutils.EnsureNeverCalled
|
||||
|
|
@ -61,21 +60,10 @@ class AboutViewTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `if open source licenses are not available, the entry is not displayed`() {
|
||||
rule.setAboutView(
|
||||
anAboutState(),
|
||||
)
|
||||
val text = rule.activity.getString(CommonStrings.common_open_source_licenses)
|
||||
rule.onNodeWithText(text).assertDoesNotExist()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `if open source licenses are available, clicking on the entry invokes the expected callback`() {
|
||||
fun `clicking on the open source licenses invokes the expected callback`() {
|
||||
ensureCalledOnce { callback ->
|
||||
rule.setAboutView(
|
||||
anAboutState(
|
||||
hasOpenSourcesLicenses = true,
|
||||
),
|
||||
anAboutState(),
|
||||
onOpenSourceLicensesClick = callback,
|
||||
)
|
||||
rule.clickOn(CommonStrings.common_open_source_licenses)
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@ import app.cash.turbine.test
|
|||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.ftue.test.FakeFtueService
|
||||
import io.element.android.features.preferences.impl.DefaultCacheService
|
||||
import io.element.android.features.roomlist.impl.migration.InMemoryMigrationScreenStore
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||
import io.element.android.libraries.push.test.FakePushService
|
||||
|
|
@ -47,10 +46,6 @@ class DefaultClearCacheUseCaseTest {
|
|||
val ftueService = FakeFtueService(
|
||||
resetLambda = resetFtueLambda,
|
||||
)
|
||||
val resetMigrationLambda = lambdaRecorder<Unit> { }
|
||||
val migrationScreenStore = InMemoryMigrationScreenStore(
|
||||
resetLambda = resetMigrationLambda,
|
||||
)
|
||||
val setIgnoreRegistrationErrorLambda = lambdaRecorder<SessionId, Boolean, Unit> { _, _ -> }
|
||||
val pushService = FakePushService(
|
||||
setIgnoreRegistrationErrorLambda = setIgnoreRegistrationErrorLambda
|
||||
|
|
@ -62,14 +57,12 @@ class DefaultClearCacheUseCaseTest {
|
|||
defaultCacheService = defaultCacheService,
|
||||
okHttpClient = { OkHttpClient.Builder().build() },
|
||||
ftueService = ftueService,
|
||||
migrationScreenStore = migrationScreenStore,
|
||||
pushService = pushService,
|
||||
)
|
||||
defaultCacheService.clearedCacheEventFlow.test {
|
||||
sut.invoke()
|
||||
clearCacheLambda.assertions().isCalledOnce()
|
||||
resetFtueLambda.assertions().isCalledOnce()
|
||||
resetMigrationLambda.assertions().isCalledOnce()
|
||||
setIgnoreRegistrationErrorLambda.assertions().isCalledOnce()
|
||||
.with(value(matrixClient.sessionId), value(false))
|
||||
assertThat(awaitItem()).isEqualTo(matrixClient.sessionId)
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
<string name="screen_bug_report_editor_supporting">"Iloji bo\'lsa, tavsifni ingliz tilida yozing."</string>
|
||||
<string name="screen_bug_report_include_crash_logs">"Buzilish jurnallarini yuboring"</string>
|
||||
<string name="screen_bug_report_include_logs">"Jurnallarga ruxsat bering"</string>
|
||||
<string name="screen_bug_report_include_screenshot">"Ekran tasvirini yuboring "</string>
|
||||
<string name="screen_bug_report_include_screenshot">"Ekran tasvirini yuboring"</string>
|
||||
<string name="screen_bug_report_logs_description">"Har bir narsa to\'ri ishlayotganiga ishonch hosil qilish uchun xabaringizga jurnallar kiritiladi. Xabarni jurnallarsiz yuborish uchun ushbu sozlamani oʻchiring."</string>
|
||||
<string name="screen_bug_report_rash_logs_alert_title">"%1$soxirgi marta ishlatilganda qulab tushdi. Biz bilan nosozlik hisobotini baham ko\'rmoqchimisiz?"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -289,6 +289,7 @@ class DefaultBugReporterTest {
|
|||
slidingSyncProxy = null,
|
||||
passphrase = null,
|
||||
sessionPath = "session",
|
||||
cachePath = "cache",
|
||||
)
|
||||
@Test
|
||||
fun `test sendBugReport error`() = runTest {
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
<string name="screen_room_details_error_muting">"Bu xona ovozini o‘chirib bo‘lmadi, qayta urinib ko‘ring."</string>
|
||||
<string name="screen_room_details_error_unmuting">"Bu xonaning ovozi yoqilmadi, qayta urinib ko‘ring."</string>
|
||||
<string name="screen_room_details_invite_people_title">"Odamlarni taklif qiling"</string>
|
||||
<string name="screen_room_details_leave_room_title">"Xonani tark etish "</string>
|
||||
<string name="screen_room_details_leave_room_title">"Xonani tark etish"</string>
|
||||
<string name="screen_room_details_notification_mode_custom">"Maxsus"</string>
|
||||
<string name="screen_room_details_notification_mode_default">"Standart"</string>
|
||||
<string name="screen_room_details_notification_title">"Bildirishnomalar"</string>
|
||||
|
|
|
|||
|
|
@ -1,25 +0,0 @@
|
|||
/*
|
||||
* 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.roomlist.api.migration
|
||||
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
|
||||
interface MigrationScreenStore {
|
||||
fun isMigrationScreenNeeded(sessionId: SessionId): Boolean
|
||||
fun setMigrationScreenShown(sessionId: SessionId)
|
||||
fun reset()
|
||||
}
|
||||
|
|
@ -84,7 +84,6 @@ dependencies {
|
|||
testImplementation(projects.services.analytics.test)
|
||||
testImplementation(projects.services.toolbox.test)
|
||||
testImplementation(projects.features.networkmonitor.test)
|
||||
testImplementation(projects.features.roomlist.test)
|
||||
testImplementation(projects.tests.testutils)
|
||||
testImplementation(projects.features.leaveroom.test)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,7 +29,6 @@ open class RoomListContentStateProvider : PreviewParameterProvider<RoomListConte
|
|||
aRoomsContentState(summaries = persistentListOf()),
|
||||
aSkeletonContentState(),
|
||||
anEmptyContentState(),
|
||||
aMigrationContentState(),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -43,8 +42,6 @@ internal fun aRoomsContentState(
|
|||
summaries = summaries,
|
||||
)
|
||||
|
||||
internal fun aMigrationContentState() = RoomListContentState.Migration
|
||||
|
||||
internal fun aSkeletonContentState() = RoomListContentState.Skeleton(16)
|
||||
|
||||
internal fun anEmptyContentState() = RoomListContentState.Empty
|
||||
|
|
|
|||
|
|
@ -42,7 +42,6 @@ import io.element.android.features.networkmonitor.api.NetworkMonitor
|
|||
import io.element.android.features.networkmonitor.api.NetworkStatus
|
||||
import io.element.android.features.roomlist.impl.datasource.RoomListDataSource
|
||||
import io.element.android.features.roomlist.impl.filters.RoomListFiltersState
|
||||
import io.element.android.features.roomlist.impl.migration.MigrationScreenState
|
||||
import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
|
||||
import io.element.android.features.roomlist.impl.search.RoomListSearchEvents
|
||||
import io.element.android.features.roomlist.impl.search.RoomListSearchState
|
||||
|
|
@ -93,7 +92,6 @@ class RoomListPresenter @Inject constructor(
|
|||
private val indicatorService: IndicatorService,
|
||||
private val filtersPresenter: Presenter<RoomListFiltersState>,
|
||||
private val searchPresenter: Presenter<RoomListSearchState>,
|
||||
private val migrationScreenPresenter: Presenter<MigrationScreenState>,
|
||||
private val sessionPreferencesStore: SessionPreferencesStore,
|
||||
private val analyticsService: AnalyticsService,
|
||||
private val acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState>,
|
||||
|
|
@ -210,7 +208,6 @@ class RoomListPresenter @Inject constructor(
|
|||
roomListDataSource.allRooms.collect { value = AsyncData.Success(it) }
|
||||
}
|
||||
val loadingState by roomListDataSource.loadingState.collectAsState()
|
||||
val showMigration = migrationScreenPresenter.present().isMigrating
|
||||
val showEmpty by remember {
|
||||
derivedStateOf {
|
||||
(loadingState as? RoomList.LoadingState.Loaded)?.numberOfRooms == 0
|
||||
|
|
@ -222,7 +219,6 @@ class RoomListPresenter @Inject constructor(
|
|||
}
|
||||
}
|
||||
return when {
|
||||
showMigration -> RoomListContentState.Migration
|
||||
showEmpty -> RoomListContentState.Empty
|
||||
showSkeleton -> RoomListContentState.Skeleton(count = 16)
|
||||
else -> {
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ data class RoomListState(
|
|||
val eventSink: (RoomListEvents) -> Unit,
|
||||
) {
|
||||
val displayFilters = contentState is RoomListContentState.Rooms
|
||||
val displayActions = contentState !is RoomListContentState.Migration
|
||||
val displayActions = true
|
||||
|
||||
sealed interface ContextMenu {
|
||||
data object Hidden : ContextMenu
|
||||
|
|
@ -72,7 +72,6 @@ enum class SecurityBannerState {
|
|||
|
||||
@Immutable
|
||||
sealed interface RoomListContentState {
|
||||
data object Migration : RoomListContentState
|
||||
data class Skeleton(val count: Int) : RoomListContentState
|
||||
data object Empty : RoomListContentState
|
||||
data class Rooms(
|
||||
|
|
|
|||
|
|
@ -50,7 +50,6 @@ open class RoomListStateProvider : PreviewParameterProvider<RoomListState> {
|
|||
aRoomListState(contentState = aRoomsContentState(securityBannerState = SecurityBannerState.RecoveryKeyConfirmation)),
|
||||
aRoomListState(contentState = anEmptyContentState()),
|
||||
aRoomListState(contentState = aSkeletonContentState()),
|
||||
aRoomListState(matrixUser = MatrixUser(userId = UserId("@id:domain")), contentState = aMigrationContentState()),
|
||||
aRoomListState(searchState = aRoomListSearchState(isSearchActive = true, query = "Test")),
|
||||
aRoomListState(contentState = aRoomsContentState(securityBannerState = SecurityBannerState.SetUpRecovery)),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -54,7 +54,6 @@ import io.element.android.features.roomlist.impl.filters.RoomListFiltersEmptySta
|
|||
import io.element.android.features.roomlist.impl.filters.RoomListFiltersState
|
||||
import io.element.android.features.roomlist.impl.filters.aRoomListFiltersState
|
||||
import io.element.android.features.roomlist.impl.filters.selection.FilterSelectionState
|
||||
import io.element.android.features.roomlist.impl.migration.MigrationScreenView
|
||||
import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
|
|
@ -78,9 +77,6 @@ fun RoomListContentView(
|
|||
) {
|
||||
Box(modifier = modifier) {
|
||||
when (contentState) {
|
||||
is RoomListContentState.Migration -> {
|
||||
MigrationScreenView(isMigrating = true)
|
||||
}
|
||||
is RoomListContentState.Skeleton -> {
|
||||
SkeletonView(
|
||||
count = contentState.count,
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue