Merge branch 'develop' into renovate/org.owasp.dependencycheck-10.x
This commit is contained in:
commit
ec87d3f51e
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
|
# Enrich gradle.properties for CI/CD
|
||||||
env:
|
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
|
CI_GRADLE_ARG_PROPERTIES: --stacktrace -Dsonar.gradle.skipCompile=true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|
@ -39,20 +39,13 @@ jobs:
|
||||||
uses: gradle/actions/setup-gradle@v4
|
uses: gradle/actions/setup-gradle@v4
|
||||||
with:
|
with:
|
||||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||||
- name: Assemble debug Gplay APK
|
- name: Assemble debug APKs
|
||||||
if: ${{ matrix.variant == 'debug' }}
|
if: ${{ matrix.variant == 'debug' }}
|
||||||
env:
|
env:
|
||||||
ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }}
|
ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }}
|
||||||
ELEMENT_ANDROID_MAPTILER_LIGHT_MAP_ID: ${{ secrets.MAPTILER_LIGHT_MAP_ID }}
|
ELEMENT_ANDROID_MAPTILER_LIGHT_MAP_ID: ${{ secrets.MAPTILER_LIGHT_MAP_ID }}
|
||||||
ELEMENT_ANDROID_MAPTILER_DARK_MAP_ID: ${{ secrets.MAPTILER_DARK_MAP_ID }}
|
ELEMENT_ANDROID_MAPTILER_DARK_MAP_ID: ${{ secrets.MAPTILER_DARK_MAP_ID }}
|
||||||
run: ./gradlew :app:assembleGplayDebug -PallWarningsAsErrors=true $CI_GRADLE_ARG_PROPERTIES
|
run: ./gradlew :app:assembleGplayDebug app:assembleFDroidDebug -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
|
|
||||||
- name: Upload debug APKs
|
- name: Upload debug APKs
|
||||||
if: ${{ matrix.variant == 'debug' }}
|
if: ${{ matrix.variant == 'debug' }}
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@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
|
# Enrich gradle.properties for CI/CD
|
||||||
env:
|
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
|
CI_GRADLE_ARG_PROPERTIES: --stacktrace -Dsonar.gradle.skipCompile=true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|
@ -61,7 +61,7 @@ jobs:
|
||||||
name: elementx-enterprise-debug
|
name: elementx-enterprise-debug
|
||||||
path: |
|
path: |
|
||||||
app/build/outputs/apk/gplay/debug/*-universal-debug.apk
|
app/build/outputs/apk/gplay/debug/*-universal-debug.apk
|
||||||
- name: Compile release sources
|
- name: Compile nightly and release sources
|
||||||
if: ${{ matrix.variant == 'release' }}
|
if: ${{ matrix.variant == 'release' }}
|
||||||
run: ./gradlew compileReleaseSources -PallWarningsAsErrors=true $CI_GRADLE_ARG_PROPERTIES
|
run: ./gradlew compileReleaseSources -PallWarningsAsErrors=true $CI_GRADLE_ARG_PROPERTIES
|
||||||
- name: Compile nightly sources
|
- 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
|
# Enrich gradle.properties for CI/CD
|
||||||
env:
|
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
|
CI_GRADLE_ARG_PROPERTIES: --stacktrace --no-daemon -Dsonar.gradle.skipCompile=true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|
@ -79,7 +79,7 @@ jobs:
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: elementx-apk-maestro
|
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'
|
if: (github.event_name == 'pull_request' && github.event.pull_request.fork == null) || github.event_name == 'workflow_dispatch'
|
||||||
with:
|
with:
|
||||||
api-key: ${{ secrets.MAESTRO_CLOUD_API_KEY }}
|
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 * * *"
|
- cron: "0 4 * * *"
|
||||||
|
|
||||||
env:
|
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
|
CI_GRADLE_ARG_PROPERTIES: --stacktrace --no-daemon -Dsonar.gradle.skipCompile=true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|
|
||||||
2
.github/workflows/nightlyReports.yml
vendored
2
.github/workflows/nightlyReports.yml
vendored
|
|
@ -8,7 +8,7 @@ on:
|
||||||
|
|
||||||
# Enrich gradle.properties for CI/CD
|
# Enrich gradle.properties for CI/CD
|
||||||
env:
|
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
|
CI_GRADLE_ARG_PROPERTIES: --stacktrace -Dsonar.gradle.skipCompile=true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|
|
||||||
2
.github/workflows/nightly_enterprise.yml
vendored
2
.github/workflows/nightly_enterprise.yml
vendored
|
|
@ -7,7 +7,7 @@ on:
|
||||||
- cron: "0 4 * * *"
|
- cron: "0 4 * * *"
|
||||||
|
|
||||||
env:
|
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
|
CI_GRADLE_ARG_PROPERTIES: --stacktrace --no-daemon -Dsonar.gradle.skipCompile=true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|
|
||||||
2
.github/workflows/quality.yml
vendored
2
.github/workflows/quality.yml
vendored
|
|
@ -9,7 +9,7 @@ on:
|
||||||
|
|
||||||
# Enrich gradle.properties for CI/CD
|
# Enrich gradle.properties for CI/CD
|
||||||
env:
|
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
|
CI_GRADLE_ARG_PROPERTIES: --stacktrace --no-daemon -Dsonar.gradle.skipCompile=true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|
|
||||||
2
.github/workflows/recordScreenshots.yml
vendored
2
.github/workflows/recordScreenshots.yml
vendored
|
|
@ -7,7 +7,7 @@ on:
|
||||||
|
|
||||||
# Enrich gradle.properties for CI/CD
|
# Enrich gradle.properties for CI/CD
|
||||||
env:
|
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:
|
jobs:
|
||||||
record:
|
record:
|
||||||
|
|
|
||||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
|
|
@ -7,7 +7,7 @@ on:
|
||||||
|
|
||||||
# Enrich gradle.properties for CI/CD
|
# Enrich gradle.properties for CI/CD
|
||||||
env:
|
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
|
CI_GRADLE_ARG_PROPERTIES: --stacktrace --no-daemon -Dsonar.gradle.skipCompile=true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|
|
||||||
14
.github/workflows/sonar.yml
vendored
14
.github/workflows/sonar.yml
vendored
|
|
@ -9,7 +9,7 @@ on:
|
||||||
|
|
||||||
# Enrich gradle.properties for CI/CD
|
# Enrich gradle.properties for CI/CD
|
||||||
env:
|
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
|
CI_GRADLE_ARG_PROPERTIES: --stacktrace --warn -Dsonar.gradle.skipCompile=true
|
||||||
GROUP: ${{ format('sonar-{0}', github.ref) }}
|
GROUP: ${{ format('sonar-{0}', github.ref) }}
|
||||||
|
|
||||||
|
|
@ -36,16 +36,8 @@ jobs:
|
||||||
uses: gradle/actions/setup-gradle@v4
|
uses: gradle/actions/setup-gradle@v4
|
||||||
with:
|
with:
|
||||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||||
- name: Build Gplay Debug
|
- name: Build debug code and test fixtures
|
||||||
run: ./gradlew :app:assembleGplayDebug $CI_GRADLE_ARG_PROPERTIES
|
run: ./gradlew assembleDebug createFullJarDebugTestFixtures :app:createFullJarGplayDebugTestFixtures $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: 🔊 Publish results to Sonar
|
- name: 🔊 Publish results to Sonar
|
||||||
env:
|
env:
|
||||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
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
|
# Enrich gradle.properties for CI/CD
|
||||||
env:
|
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
|
CI_GRADLE_ARG_PROPERTIES: --stacktrace -Dsonar.gradle.skipCompile=true
|
||||||
|
|
||||||
jobs:
|
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)
|
Changes in Element X v0.5.1 (2024-08-28)
|
||||||
=========================================
|
=========================================
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,15 +17,19 @@
|
||||||
@file:Suppress("UnstableApiUsage")
|
@file:Suppress("UnstableApiUsage")
|
||||||
|
|
||||||
import com.android.build.api.variant.FilterConfiguration.FilterType.ABI
|
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.allEnterpriseImpl
|
||||||
import extension.allFeaturesImpl
|
import extension.allFeaturesImpl
|
||||||
import extension.allLibrariesImpl
|
import extension.allLibrariesImpl
|
||||||
import extension.allServicesImpl
|
import extension.allServicesImpl
|
||||||
import extension.gitBranchName
|
|
||||||
import extension.gitRevision
|
|
||||||
import extension.koverDependencies
|
import extension.koverDependencies
|
||||||
import extension.locales
|
import extension.locales
|
||||||
import extension.setupKover
|
import extension.setupKover
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("io.element.android-compose-application")
|
id("io.element.android-compose-application")
|
||||||
|
|
@ -36,7 +40,8 @@ plugins {
|
||||||
id(libs.plugins.firebaseAppDistribution.get().pluginId)
|
id(libs.plugins.firebaseAppDistribution.get().pluginId)
|
||||||
alias(libs.plugins.knit)
|
alias(libs.plugins.knit)
|
||||||
id("kotlin-parcelize")
|
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
|
// To be able to update the firebase.xml files, uncomment and build the project
|
||||||
// id("com.google.gms.google-services")
|
// id("com.google.gms.google-services")
|
||||||
}
|
}
|
||||||
|
|
@ -61,9 +66,6 @@ android {
|
||||||
abiFilters += listOf("armeabi-v7a", "x86", "arm64-v8a", "x86_64")
|
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
|
// Ref: https://developer.android.com/studio/build/configure-apk-splits.html#configure-abi-split
|
||||||
splits {
|
splits {
|
||||||
// Configures multiple APKs based on ABI.
|
// Configures multiple APKs based on ABI.
|
||||||
|
|
@ -215,6 +217,9 @@ androidComponents {
|
||||||
output.versionCode.set((output.versionCode.orNull ?: 0) * 10 + abiCode)
|
output.versionCode.set((output.versionCode.orNull ?: 0) * 10 + abiCode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val reportingExtension: ReportingExtension = project.extensions.getByType(ReportingExtension::class.java)
|
||||||
|
configureLicensesTasks(reportingExtension)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Knit
|
// Knit
|
||||||
|
|
@ -259,8 +264,6 @@ dependencies {
|
||||||
// Comment to not include unified push in the project
|
// Comment to not include unified push in the project
|
||||||
implementation(projects.libraries.pushproviders.unifiedpush)
|
implementation(projects.libraries.pushproviders.unifiedpush)
|
||||||
|
|
||||||
"gplayImplementation"(libs.play.services.oss.licenses)
|
|
||||||
|
|
||||||
implementation(libs.appyx.core)
|
implementation(libs.appyx.core)
|
||||||
implementation(libs.androidx.splash)
|
implementation(libs.androidx.splash)
|
||||||
implementation(libs.androidx.core)
|
implementation(libs.androidx.core)
|
||||||
|
|
@ -291,3 +294,51 @@ dependencies {
|
||||||
|
|
||||||
koverDependencies()
|
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 {
|
buildscript {
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath(libs.kotlin.gradle.plugin)
|
classpath(libs.kotlin.gradle.plugin)
|
||||||
classpath(libs.gms.google.services)
|
classpath(libs.gms.google.services)
|
||||||
classpath(libs.oss.licenses.plugin)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -202,24 +198,6 @@ subprojects {
|
||||||
tasks.findByName("recordPaparazziRelease")?.dependsOn(removeOldScreenshotsTask)
|
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 {
|
subprojects {
|
||||||
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
|
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
|
||||||
kotlinOptions {
|
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.content.IntentCompat
|
||||||
import androidx.core.util.Consumer
|
import androidx.core.util.Consumer
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import io.element.android.features.call.api.CallType
|
import io.element.android.features.call.api.CallType
|
||||||
import io.element.android.features.call.impl.DefaultElementCallEntryPoint
|
import io.element.android.features.call.impl.DefaultElementCallEntryPoint
|
||||||
import io.element.android.features.call.impl.di.CallBindings
|
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.architecture.bindings
|
||||||
import io.element.android.libraries.designsystem.theme.ElementThemeApp
|
import io.element.android.libraries.designsystem.theme.ElementThemeApp
|
||||||
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
|
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
|
@ -128,12 +130,14 @@ class ElementCallActivity :
|
||||||
private fun ListenToAndroidEvents(pipState: PictureInPictureState) {
|
private fun ListenToAndroidEvents(pipState: PictureInPictureState) {
|
||||||
val pipEventSink by rememberUpdatedState(pipState.eventSink)
|
val pipEventSink by rememberUpdatedState(pipState.eventSink)
|
||||||
DisposableEffect(Unit) {
|
DisposableEffect(Unit) {
|
||||||
val onUserLeaveHintListener = Runnable {
|
val listener = Runnable {
|
||||||
pipEventSink(PictureInPictureEvents.EnterPictureInPicture)
|
pipEventSink(PictureInPictureEvents.EnterPictureInPicture)
|
||||||
}
|
}
|
||||||
addOnUserLeaveHintListener(onUserLeaveHintListener)
|
lifecycleScope.launch {
|
||||||
|
addOnUserLeaveHintListener(listener)
|
||||||
|
}
|
||||||
onDispose {
|
onDispose {
|
||||||
removeOnUserLeaveHintListener(onUserLeaveHintListener)
|
removeOnUserLeaveHintListener(listener)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
DisposableEffect(Unit) {
|
DisposableEffect(Unit) {
|
||||||
|
|
|
||||||
|
|
@ -3,5 +3,5 @@
|
||||||
<string name="call_foreground_service_channel_title_android">"Chamada em curso"</string>
|
<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_message_android">"Toca para voltar à chamada"</string>
|
||||||
<string name="call_foreground_service_title_android">"☎️ Chamada em curso"</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>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@ class FtueSessionVerificationFlowNode @AssistedInject constructor(
|
||||||
private val secureBackupEntryPoint: SecureBackupEntryPoint,
|
private val secureBackupEntryPoint: SecureBackupEntryPoint,
|
||||||
) : BaseFlowNode<FtueSessionVerificationFlowNode.NavTarget>(
|
) : BaseFlowNode<FtueSessionVerificationFlowNode.NavTarget>(
|
||||||
backstack = BackStack(
|
backstack = BackStack(
|
||||||
initialElement = NavTarget.Root,
|
initialElement = NavTarget.Root(showDeviceVerifiedScreen = false),
|
||||||
savedStateMap = buildContext.savedStateMap,
|
savedStateMap = buildContext.savedStateMap,
|
||||||
),
|
),
|
||||||
buildContext = buildContext,
|
buildContext = buildContext,
|
||||||
|
|
@ -54,7 +54,7 @@ class FtueSessionVerificationFlowNode @AssistedInject constructor(
|
||||||
) {
|
) {
|
||||||
sealed interface NavTarget : Parcelable {
|
sealed interface NavTarget : Parcelable {
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data object Root : NavTarget
|
data class Root(val showDeviceVerifiedScreen: Boolean) : NavTarget
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data object EnterRecoveryKey : NavTarget
|
data object EnterRecoveryKey : NavTarget
|
||||||
|
|
@ -71,7 +71,7 @@ class FtueSessionVerificationFlowNode @AssistedInject constructor(
|
||||||
override fun onDone() {
|
override fun onDone() {
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
// Move to the completed state view in the verification flow
|
// 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) {
|
return when (navTarget) {
|
||||||
is NavTarget.Root -> {
|
is NavTarget.Root -> {
|
||||||
verifySessionEntryPoint.nodeBuilder(this, buildContext)
|
verifySessionEntryPoint.nodeBuilder(this, buildContext)
|
||||||
|
.params(VerifySessionEntryPoint.Params(navTarget.showDeviceVerifiedScreen))
|
||||||
.callback(object : VerifySessionEntryPoint.Callback {
|
.callback(object : VerifySessionEntryPoint.Callback {
|
||||||
override fun onEnterRecoveryKey() {
|
override fun onEnterRecoveryKey() {
|
||||||
backstack.push(NavTarget.EnterRecoveryKey)
|
backstack.push(NavTarget.EnterRecoveryKey)
|
||||||
|
|
|
||||||
|
|
@ -14,15 +14,15 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
plugins {
|
plugins {
|
||||||
id("io.element.android-library")
|
id("io.element.android-compose-library")
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "io.element.android.features.roomlist.test"
|
namespace = "io.element.android.features.licenses.api"
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(projects.features.roomlist.api)
|
implementation(projects.libraries.architecture)
|
||||||
implementation(projects.libraries.matrix.api)
|
implementation(projects.libraries.designsystem)
|
||||||
implementation(projects.tests.testutils)
|
implementation(projects.libraries.uiStrings)
|
||||||
}
|
}
|
||||||
|
|
@ -14,11 +14,11 @@
|
||||||
* limitations under the License.
|
* 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 {
|
interface OpenSourceLicensesEntryPoint {
|
||||||
val hasOpenSourceLicenses: Boolean
|
fun getNode(node: Node, buildContext: BuildContext): Node
|
||||||
fun navigateToOpenSourceLicenses(activity: Activity)
|
|
||||||
}
|
}
|
||||||
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.
|
* 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 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 io.element.android.libraries.di.AppScope
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@ContributesBinding(AppScope::class)
|
@ContributesBinding(AppScope::class)
|
||||||
class FdroidOpenSourceLicensesProvider @Inject constructor() : OpenSourceLicensesProvider {
|
class DefaultOpenSourcesLicensesEntryPoint @Inject constructor() : OpenSourceLicensesEntryPoint {
|
||||||
override val hasOpenSourceLicenses: Boolean = false
|
override fun getNode(node: Node, buildContext: BuildContext): Node {
|
||||||
|
return node.createNode<DependenciesFlowNode>(buildContext)
|
||||||
override fun navigateToOpenSourceLicenses(activity: Activity) {
|
|
||||||
error("Not supported, please ensure that hasOpenSourcesLicenses is true before calling this method")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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.
|
* 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.licenses.impl.model.DependencyLicenseItem
|
||||||
import io.element.android.features.preferences.api.OpenSourceLicensesProvider
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
class FakeOpenSourceLicensesProvider(
|
data class DependencyLicensesListState(
|
||||||
override val hasOpenSourceLicenses: Boolean,
|
val licenses: AsyncData<ImmutableList<DependencyLicenseItem>>,
|
||||||
) : OpenSourceLicensesProvider {
|
)
|
||||||
override fun navigateToOpenSourceLicenses(activity: Activity) = Unit
|
|
||||||
}
|
|
||||||
|
|
@ -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 com.squareup.anvil.annotations.ContributesTo
|
||||||
import io.element.android.features.lockscreen.impl.unlock.activity.PinUnlockActivity
|
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 {
|
interface PinUnlockBindings {
|
||||||
fun inject(activity: PinUnlockActivity)
|
fun inject(activity: PinUnlockActivity)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ sealed class ChangeServerError : Throwable() {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun from(error: Throwable): ChangeServerError = when (error) {
|
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)
|
else -> Error(R.string.screen_change_server_error_invalid_homeserver)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ class ErrorFormatterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `loginError - invalid auth error returns unknown error message`() {
|
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)
|
assertThat(loginError(error)).isEqualTo(CommonStrings.error_unknown)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,27 +17,25 @@
|
||||||
package io.element.android.features.logout.impl
|
package io.element.android.features.logout.impl
|
||||||
|
|
||||||
import com.squareup.anvil.annotations.ContributesBinding
|
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.features.logout.api.LogoutUseCase
|
||||||
import io.element.android.libraries.di.AppScope
|
import io.element.android.libraries.di.AppScope
|
||||||
import io.element.android.libraries.matrix.api.MatrixClientProvider
|
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(
|
@ContributesBinding(AppScope::class)
|
||||||
@Assisted private val sessionId: String,
|
class DefaultLogoutUseCase @Inject constructor(
|
||||||
|
private val authenticationService: MatrixAuthenticationService,
|
||||||
private val matrixClientProvider: MatrixClientProvider,
|
private val matrixClientProvider: MatrixClientProvider,
|
||||||
) : LogoutUseCase {
|
) : LogoutUseCase {
|
||||||
@ContributesBinding(AppScope::class)
|
|
||||||
@AssistedFactory
|
|
||||||
interface Factory : LogoutUseCase.Factory {
|
|
||||||
override fun create(sessionId: String): DefaultLogoutUseCase
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun logout(ignoreSdkError: Boolean): String? {
|
override suspend fun logout(ignoreSdkError: Boolean): String? {
|
||||||
val matrixClient = matrixClientProvider.getOrRestore(SessionId(sessionId)).getOrThrow()
|
val currentSession = authenticationService.getLatestSessionId()
|
||||||
val result = matrixClient.logout(ignoreSdkError = ignoreSdkError)
|
return if (currentSession != null) {
|
||||||
return result
|
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,
|
ignoreSdkError: Boolean,
|
||||||
) = launch {
|
) = launch {
|
||||||
suspend {
|
suspend {
|
||||||
matrixClient.logout(ignoreSdkError)
|
matrixClient.logout(userInitiated = true, ignoreSdkError)
|
||||||
}.runCatchingUpdatingState(logoutAction)
|
}.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,
|
ignoreSdkError: Boolean,
|
||||||
) = launch {
|
) = launch {
|
||||||
suspend {
|
suspend {
|
||||||
matrixClient.logout(ignoreSdkError)
|
matrixClient.logout(userInitiated = true, ignoreSdkError)
|
||||||
}.runCatchingUpdatingState(logoutAction)
|
}.runCatchingUpdatingState(logoutAction)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -144,7 +144,7 @@ class LogoutPresenterTest {
|
||||||
@Test
|
@Test
|
||||||
fun `present - logout with error then cancel`() = runTest {
|
fun `present - logout with error then cancel`() = runTest {
|
||||||
val matrixClient = FakeMatrixClient().apply {
|
val matrixClient = FakeMatrixClient().apply {
|
||||||
logoutLambda = { _ ->
|
logoutLambda = { _, _ ->
|
||||||
throw A_THROWABLE
|
throw A_THROWABLE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -172,7 +172,7 @@ class LogoutPresenterTest {
|
||||||
@Test
|
@Test
|
||||||
fun `present - logout with error then force`() = runTest {
|
fun `present - logout with error then force`() = runTest {
|
||||||
val matrixClient = FakeMatrixClient().apply {
|
val matrixClient = FakeMatrixClient().apply {
|
||||||
logoutLambda = { ignoreSdkError ->
|
logoutLambda = { ignoreSdkError, _ ->
|
||||||
if (!ignoreSdkError) {
|
if (!ignoreSdkError) {
|
||||||
throw A_THROWABLE
|
throw A_THROWABLE
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -125,7 +125,7 @@ class DefaultDirectLogoutPresenterTest {
|
||||||
@Test
|
@Test
|
||||||
fun `present - logout with error then cancel`() = runTest {
|
fun `present - logout with error then cancel`() = runTest {
|
||||||
val matrixClient = FakeMatrixClient().apply {
|
val matrixClient = FakeMatrixClient().apply {
|
||||||
logoutLambda = { _ ->
|
logoutLambda = { _, _ ->
|
||||||
throw A_THROWABLE
|
throw A_THROWABLE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -153,7 +153,7 @@ class DefaultDirectLogoutPresenterTest {
|
||||||
@Test
|
@Test
|
||||||
fun `present - logout with error then force`() = runTest {
|
fun `present - logout with error then force`() = runTest {
|
||||||
val matrixClient = FakeMatrixClient().apply {
|
val matrixClient = FakeMatrixClient().apply {
|
||||||
logoutLambda = { ignoreSdkError ->
|
logoutLambda = { ignoreSdkError, _ ->
|
||||||
if (!ignoreSdkError) {
|
if (!ignoreSdkError) {
|
||||||
throw A_THROWABLE
|
throw A_THROWABLE
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -72,6 +72,7 @@ dependencies {
|
||||||
implementation(libs.androidx.constraintlayout.compose)
|
implementation(libs.androidx.constraintlayout.compose)
|
||||||
implementation(libs.androidx.media3.exoplayer)
|
implementation(libs.androidx.media3.exoplayer)
|
||||||
implementation(libs.androidx.media3.ui)
|
implementation(libs.androidx.media3.ui)
|
||||||
|
implementation(libs.sigpwned.emoji4j)
|
||||||
implementation(libs.vanniktech.blurhash)
|
implementation(libs.vanniktech.blurhash)
|
||||||
implementation(libs.telephoto.zoomableimage)
|
implementation(libs.telephoto.zoomableimage)
|
||||||
implementation(libs.matrix.emojibase.bindings)
|
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.actionlist.model.TimelineItemAction
|
||||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
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 {
|
sealed interface MessagesEvents {
|
||||||
data class HandleAction(val action: TimelineItemAction, val event: TimelineItem.Event) : 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 class InviteDialogDismissed(val action: InviteDialogAction) : MessagesEvents
|
||||||
data object Dismiss : 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.designsystem.utils.snackbar.collectSnackbarMessageAsState
|
||||||
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
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.permalink.PermalinkParser
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoomInfo
|
import io.element.android.libraries.matrix.api.room.MatrixRoomInfo
|
||||||
|
|
@ -192,7 +192,7 @@ class MessagesPresenter @AssistedInject constructor(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
is MessagesEvents.ToggleReaction -> {
|
is MessagesEvents.ToggleReaction -> {
|
||||||
localCoroutineScope.toggleReaction(event.emoji, event.eventId)
|
localCoroutineScope.toggleReaction(event.emoji, event.uniqueId)
|
||||||
}
|
}
|
||||||
is MessagesEvents.InviteDialogDismissed -> {
|
is MessagesEvents.InviteDialogDismissed -> {
|
||||||
hasDismissedInviteDialog = true
|
hasDismissedInviteDialog = true
|
||||||
|
|
@ -313,10 +313,10 @@ class MessagesPresenter @AssistedInject constructor(
|
||||||
|
|
||||||
private fun CoroutineScope.toggleReaction(
|
private fun CoroutineScope.toggleReaction(
|
||||||
emoji: String,
|
emoji: String,
|
||||||
eventId: EventId,
|
uniqueId: UniqueId,
|
||||||
) = launch(dispatchers.io) {
|
) = launch(dispatchers.io) {
|
||||||
timelineController.invokeOnCurrentTimeline {
|
timelineController.invokeOnCurrentTimeline {
|
||||||
toggleReaction(emoji, eventId)
|
toggleReaction(emoji, uniqueId)
|
||||||
.onFailure { Timber.e(it) }
|
.onFailure { Timber.e(it) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -175,8 +175,7 @@ fun MessagesView(
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onEmojiReactionClick(emoji: String, event: TimelineItem.Event) {
|
fun onEmojiReactionClick(emoji: String, event: TimelineItem.Event) {
|
||||||
if (event.eventId == null) return
|
state.eventSink(MessagesEvents.ToggleReaction(emoji, event.id))
|
||||||
state.eventSink(MessagesEvents.ToggleReaction(emoji, event.eventId))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onEmojiReactionLongClick(emoji: String, event: TimelineItem.Event) {
|
fun onEmojiReactionLongClick(emoji: String, event: TimelineItem.Event) {
|
||||||
|
|
@ -248,7 +247,6 @@ fun MessagesView(
|
||||||
state = state.actionListState,
|
state = state.actionListState,
|
||||||
onSelectAction = ::onActionSelected,
|
onSelectAction = ::onActionSelected,
|
||||||
onCustomReactionClick = { event ->
|
onCustomReactionClick = { event ->
|
||||||
if (event.eventId == null) return@ActionListView
|
|
||||||
state.customReactionState.eventSink(CustomReactionEvents.ShowCustomReactionSheet(event))
|
state.customReactionState.eventSink(CustomReactionEvents.ShowCustomReactionSheet(event))
|
||||||
},
|
},
|
||||||
onEmojiReactionClick = ::onEmojiReactionClick,
|
onEmojiReactionClick = ::onEmojiReactionClick,
|
||||||
|
|
@ -256,8 +254,8 @@ fun MessagesView(
|
||||||
|
|
||||||
CustomReactionBottomSheet(
|
CustomReactionBottomSheet(
|
||||||
state = state.customReactionState,
|
state = state.customReactionState,
|
||||||
onSelectEmoji = { eventId, emoji ->
|
onSelectEmoji = { uniqueId, emoji ->
|
||||||
state.eventSink(MessagesEvents.ToggleReaction(emoji.unicode, eventId))
|
state.eventSink(MessagesEvents.ToggleReaction(emoji.unicode, uniqueId))
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -106,7 +106,6 @@ class ActionListPresenter @Inject constructor(
|
||||||
isEventPinned = pinnedEventIds.contains(timelineItem.eventId),
|
isEventPinned = pinnedEventIds.contains(timelineItem.eventId),
|
||||||
)
|
)
|
||||||
val displayEmojiReactions = usersEventPermissions.canSendReaction &&
|
val displayEmojiReactions = usersEventPermissions.canSendReaction &&
|
||||||
timelineItem.isRemote &&
|
|
||||||
timelineItem.content.canReact()
|
timelineItem.content.canReact()
|
||||||
if (actions.isNotEmpty() || displayEmojiReactions) {
|
if (actions.isNotEmpty() || displayEmojiReactions) {
|
||||||
target.value = ActionListState.Target.Success(
|
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.bool.orFalse
|
||||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
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.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.MatrixRoom
|
||||||
import io.element.android.libraries.matrix.api.room.MessageEventType
|
import io.element.android.libraries.matrix.api.room.MessageEventType
|
||||||
import io.element.android.libraries.matrix.api.room.isDm
|
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 userHasPermissionToSendMessage by room.canSendMessageAsState(type = MessageEventType.ROOM_MESSAGE, updateKey = syncUpdateFlow.value)
|
||||||
val userHasPermissionToSendReaction by room.canSendMessageAsState(type = MessageEventType.REACTION, 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 newEventState = remember { mutableStateOf(NewEventState.None) }
|
||||||
val messageShield: MutableState<MessageShield?> = remember { mutableStateOf(null) }
|
val messageShield: MutableState<MessageShield?> = remember { mutableStateOf(null) }
|
||||||
|
|
@ -242,7 +243,7 @@ class TimelinePresenter @AssistedInject constructor(
|
||||||
*/
|
*/
|
||||||
private suspend fun computeNewItemState(
|
private suspend fun computeNewItemState(
|
||||||
timelineItems: ImmutableList<TimelineItem>,
|
timelineItems: ImmutableList<TimelineItem>,
|
||||||
prevMostRecentItemId: MutableState<String?>,
|
prevMostRecentItemId: MutableState<UniqueId?>,
|
||||||
newEventState: MutableState<NewEventState>
|
newEventState: MutableState<NewEventState>
|
||||||
) = withContext(dispatchers.computation) {
|
) = withContext(dispatchers.computation) {
|
||||||
// FromMe is prioritized over FromOther, so skip if we already have a FromMe
|
// 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.designsystem.components.avatar.AvatarSize
|
||||||
import io.element.android.libraries.matrix.api.core.EventId
|
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.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.core.UserId
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo
|
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.LocalEventSendState
|
||||||
|
|
@ -122,7 +123,10 @@ internal fun aTimelineItemList(content: TimelineItemEventContent): ImmutableList
|
||||||
}
|
}
|
||||||
|
|
||||||
fun aTimelineItemDaySeparator(): TimelineItem.Virtual {
|
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(
|
internal fun aTimelineItemEvent(
|
||||||
|
|
@ -144,7 +148,7 @@ internal fun aTimelineItemEvent(
|
||||||
messageShield: MessageShield? = null,
|
messageShield: MessageShield? = null,
|
||||||
): TimelineItem.Event {
|
): TimelineItem.Event {
|
||||||
return TimelineItem.Event(
|
return TimelineItem.Event(
|
||||||
id = UUID.randomUUID().toString(),
|
id = UniqueId(UUID.randomUUID().toString()),
|
||||||
eventId = eventId,
|
eventId = eventId,
|
||||||
transactionId = transactionId,
|
transactionId = transactionId,
|
||||||
senderId = UserId("@senderId:domain"),
|
senderId = UserId("@senderId:domain"),
|
||||||
|
|
@ -210,7 +214,7 @@ internal fun aTimelineItemReadReceipts(
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun aGroupedEvents(
|
internal fun aGroupedEvents(
|
||||||
id: Long = 0,
|
id: UniqueId = UniqueId("0"),
|
||||||
withReadReceipts: Boolean = false,
|
withReadReceipts: Boolean = false,
|
||||||
): TimelineItem.GroupedEvents {
|
): TimelineItem.GroupedEvents {
|
||||||
val event1 = aTimelineItemEvent(
|
val event1 = aTimelineItemEvent(
|
||||||
|
|
@ -231,7 +235,7 @@ internal fun aGroupedEvents(
|
||||||
)
|
)
|
||||||
val events = listOf(event1, event2)
|
val events = listOf(event1, event2)
|
||||||
return TimelineItem.GroupedEvents(
|
return TimelineItem.GroupedEvents(
|
||||||
id = id.toString(),
|
id = id,
|
||||||
events = events.toImmutableList(),
|
events = events.toImmutableList(),
|
||||||
aggregatedReadReceipts = events.flatMap { it.readReceiptState.receipts }.toImmutableList(),
|
aggregatedReadReceipts = events.flatMap { it.readReceiptState.receipts }.toImmutableList(),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,7 @@ fun TimelineItemGroupedEventsRow(
|
||||||
eventSink: (TimelineEvents.EventFromTimelineItem) -> Unit,
|
eventSink: (TimelineEvents.EventFromTimelineItem) -> Unit,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
val isExpanded = rememberSaveable(key = timelineItem.identifier()) { mutableStateOf(false) }
|
val isExpanded = rememberSaveable(key = timelineItem.identifier().value) { mutableStateOf(false) }
|
||||||
|
|
||||||
fun onExpandGroupClick() {
|
fun onExpandGroupClick() {
|
||||||
isExpanded.value = !isExpanded.value
|
isExpanded.value = !isExpanded.value
|
||||||
|
|
|
||||||
|
|
@ -25,13 +25,13 @@ import androidx.compose.ui.Modifier
|
||||||
import io.element.android.emojibasebindings.Emoji
|
import io.element.android.emojibasebindings.Emoji
|
||||||
import io.element.android.libraries.designsystem.theme.components.ModalBottomSheet
|
import io.element.android.libraries.designsystem.theme.components.ModalBottomSheet
|
||||||
import io.element.android.libraries.designsystem.theme.components.hide
|
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)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun CustomReactionBottomSheet(
|
fun CustomReactionBottomSheet(
|
||||||
state: CustomReactionState,
|
state: CustomReactionState,
|
||||||
onSelectEmoji: (EventId, Emoji) -> Unit,
|
onSelectEmoji: (UniqueId, Emoji) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val sheetState = rememberModalBottomSheetState()
|
val sheetState = rememberModalBottomSheetState()
|
||||||
|
|
@ -43,10 +43,10 @@ fun CustomReactionBottomSheet(
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onEmojiSelectedDismiss(emoji: Emoji) {
|
fun onEmojiSelectedDismiss(emoji: Emoji) {
|
||||||
if (target?.event?.eventId == null) return
|
if (target?.event == null) return
|
||||||
sheetState.hide(coroutineScope) {
|
sheetState.hide(coroutineScope) {
|
||||||
state.eventSink(CustomReactionEvents.DismissCustomReactionSheet)
|
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.components.layout.ContentAvoidingLayoutData
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContent
|
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.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.ElementPreview
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
|
|
@ -54,9 +55,15 @@ fun TimelineItemTextView(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
onContentLayoutChange: (ContentAvoidingLayoutData) -> Unit = {},
|
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(
|
CompositionLocalProvider(
|
||||||
LocalContentColor provides ElementTheme.colors.textPrimary,
|
LocalContentColor provides ElementTheme.colors.textPrimary,
|
||||||
LocalTextStyle provides ElementTheme.typography.fontBodyLgRegular
|
LocalTextStyle provides textStyle
|
||||||
) {
|
) {
|
||||||
val body = getTextWithResolvedMentions(content)
|
val body = getTextWithResolvedMentions(content)
|
||||||
Box(modifier.semantics { contentDescription = content.plainText }) {
|
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.features.messages.impl.timeline.model.TimelineItem
|
||||||
import io.element.android.libraries.di.RoomScope
|
import io.element.android.libraries.di.RoomScope
|
||||||
import io.element.android.libraries.di.SingleIn
|
import io.element.android.libraries.di.SingleIn
|
||||||
|
import io.element.android.libraries.matrix.api.core.UniqueId
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
|
@ -71,7 +72,7 @@ private fun MutableList<TimelineItem>.addGroup(
|
||||||
val groupId = groupIds.getOrPutGroupId(groupOfItems)
|
val groupId = groupIds.getOrPutGroupId(groupOfItems)
|
||||||
add(
|
add(
|
||||||
TimelineItem.GroupedEvents(
|
TimelineItem.GroupedEvents(
|
||||||
id = groupId,
|
id = UniqueId(groupId),
|
||||||
events = groupOfItems.toImmutableList(),
|
events = groupOfItems.toImmutableList(),
|
||||||
aggregatedReadReceipts = groupOfItems.flatMap { it.readReceiptState.receipts }.toImmutableList()
|
aggregatedReadReceipts = groupOfItems.flatMap { it.readReceiptState.receipts }.toImmutableList()
|
||||||
)
|
)
|
||||||
|
|
@ -83,15 +84,15 @@ private fun MutableMap<String, String>.getOrPutGroupId(timelineItems: List<Timel
|
||||||
assert(timelineItems.isNotEmpty())
|
assert(timelineItems.isNotEmpty())
|
||||||
for (item in timelineItems) {
|
for (item in timelineItems) {
|
||||||
val itemIdentifier = item.identifier()
|
val itemIdentifier = item.identifier()
|
||||||
if (this.contains(itemIdentifier)) {
|
if (this.contains(itemIdentifier.value)) {
|
||||||
return this[itemIdentifier]!!
|
return this[itemIdentifier.value]!!
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val timelineItem = timelineItems.first()
|
val timelineItem = timelineItems.first()
|
||||||
return computeGroupIdWith(timelineItem).also { groupId ->
|
return computeGroupIdWith(timelineItem).value.also { groupId ->
|
||||||
this[timelineItem.identifier()] = groupId
|
this[timelineItem.identifier().value] = groupId
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@VisibleForTesting
|
@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,
|
count: Int = 1,
|
||||||
isHighlighted: Boolean = false,
|
isHighlighted: Boolean = false,
|
||||||
): AggregatedReaction {
|
): AggregatedReaction {
|
||||||
|
val timeFormatter = DateFormat.getTimeInstance(DateFormat.SHORT, java.util.Locale.US)
|
||||||
|
val date = Date(1_689_061_264L)
|
||||||
val senders = buildList {
|
val senders = buildList {
|
||||||
repeat(count) { index ->
|
repeat(count) { index ->
|
||||||
val timeFormatter = DateFormat.getTimeInstance(DateFormat.SHORT)
|
|
||||||
val date = Date(1_689_061_264L)
|
|
||||||
add(
|
add(
|
||||||
AggregatedReactionSender(
|
AggregatedReactionSender(
|
||||||
senderId = if (isHighlighted && index == 0) userId else UserId("@user$index:server.org"),
|
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.designsystem.components.avatar.AvatarData
|
||||||
import io.element.android.libraries.matrix.api.core.EventId
|
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.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.core.UserId
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo
|
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.LocalEventSendState
|
||||||
|
|
@ -36,7 +37,7 @@ import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
sealed interface TimelineItem {
|
sealed interface TimelineItem {
|
||||||
fun identifier(): String = when (this) {
|
fun identifier(): UniqueId = when (this) {
|
||||||
is Event -> id
|
is Event -> id
|
||||||
is Virtual -> id
|
is Virtual -> id
|
||||||
is GroupedEvents -> id
|
is GroupedEvents -> id
|
||||||
|
|
@ -58,13 +59,13 @@ sealed interface TimelineItem {
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
data class Virtual(
|
data class Virtual(
|
||||||
val id: String,
|
val id: UniqueId,
|
||||||
val model: TimelineItemVirtualModel
|
val model: TimelineItemVirtualModel
|
||||||
) : TimelineItem
|
) : TimelineItem
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
data class Event(
|
data class Event(
|
||||||
val id: String,
|
val id: UniqueId,
|
||||||
// Note: eventId can be null when the event is a local echo
|
// Note: eventId can be null when the event is a local echo
|
||||||
val eventId: EventId? = null,
|
val eventId: EventId? = null,
|
||||||
val transactionId: TransactionId? = null,
|
val transactionId: TransactionId? = null,
|
||||||
|
|
@ -101,7 +102,7 @@ sealed interface TimelineItem {
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
data class GroupedEvents(
|
data class GroupedEvents(
|
||||||
val id: String,
|
val id: UniqueId,
|
||||||
val events: ImmutableList<Event>,
|
val events: ImmutableList<Event>,
|
||||||
val aggregatedReadReceipts: ImmutableList<ReadReceiptData>,
|
val aggregatedReadReceipts: ImmutableList<ReadReceiptData>,
|
||||||
) : TimelineItem
|
) : TimelineItem
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,7 @@ class TimelineItemEventContentProvider : PreviewParameterProvider<TimelineItemEv
|
||||||
aTimelineItemTextContent(),
|
aTimelineItemTextContent(),
|
||||||
aTimelineItemUnknownContent(),
|
aTimelineItemUnknownContent(),
|
||||||
aTimelineItemTextContent().copy(isEdited = true),
|
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.featureflag.test.FakeFeatureFlagService
|
||||||
import io.element.android.libraries.matrix.api.core.EventId
|
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.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.core.UserId
|
||||||
import io.element.android.libraries.matrix.api.media.MediaSource
|
import io.element.android.libraries.matrix.api.media.MediaSource
|
||||||
import io.element.android.libraries.matrix.api.permalink.PermalinkParser
|
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.MessageEventType
|
||||||
import io.element.android.libraries.matrix.api.room.RoomMembershipState
|
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_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_ROOM_ID
|
||||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||||
import io.element.android.libraries.matrix.test.A_SESSION_ID_2
|
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_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.core.aBuildMeta
|
||||||
import io.element.android.libraries.matrix.test.permalink.FakePermalinkBuilder
|
import io.element.android.libraries.matrix.test.permalink.FakePermalinkBuilder
|
||||||
import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser
|
import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser
|
||||||
|
|
@ -197,8 +198,8 @@ class MessagesPresenterTest {
|
||||||
@Test
|
@Test
|
||||||
fun `present - handle toggling a reaction`() = runTest {
|
fun `present - handle toggling a reaction`() = runTest {
|
||||||
val coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true)
|
val coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true)
|
||||||
val toggleReactionSuccess = lambdaRecorder { _: String, _: EventId -> Result.success(Unit) }
|
val toggleReactionSuccess = lambdaRecorder { _: String, _: UniqueId -> Result.success(Unit) }
|
||||||
val toggleReactionFailure = lambdaRecorder { _: String, _: EventId -> Result.failure<Unit>(IllegalStateException("Failed to send reaction")) }
|
val toggleReactionFailure = lambdaRecorder { _: String, _: UniqueId -> Result.failure<Unit>(IllegalStateException("Failed to send reaction")) }
|
||||||
|
|
||||||
val timeline = FakeTimeline().apply {
|
val timeline = FakeTimeline().apply {
|
||||||
this.toggleReactionLambda = toggleReactionSuccess
|
this.toggleReactionLambda = toggleReactionSuccess
|
||||||
|
|
@ -218,25 +219,25 @@ class MessagesPresenterTest {
|
||||||
}.test {
|
}.test {
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
val initialState = awaitItem()
|
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
|
// No crashes when sending a reaction failed
|
||||||
timeline.apply { toggleReactionLambda = toggleReactionFailure }
|
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)
|
assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None)
|
||||||
|
|
||||||
assert(toggleReactionSuccess)
|
assert(toggleReactionSuccess)
|
||||||
.isCalledOnce()
|
.isCalledOnce()
|
||||||
.with(value("👍"), value(AN_EVENT_ID))
|
.with(value("👍"), value(A_UNIQUE_ID))
|
||||||
assert(toggleReactionFailure)
|
assert(toggleReactionFailure)
|
||||||
.isCalledOnce()
|
.isCalledOnce()
|
||||||
.with(value("👍"), value(AN_EVENT_ID))
|
.with(value("👍"), value(A_UNIQUE_ID))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - handle toggling a reaction twice`() = runTest {
|
fun `present - handle toggling a reaction twice`() = runTest {
|
||||||
val coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true)
|
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 {
|
val timeline = FakeTimeline().apply {
|
||||||
this.toggleReactionLambda = toggleReactionSuccess
|
this.toggleReactionLambda = toggleReactionSuccess
|
||||||
|
|
@ -255,13 +256,13 @@ class MessagesPresenterTest {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitFirstItem()
|
val initialState = awaitFirstItem()
|
||||||
initialState.eventSink.invoke(MessagesEvents.ToggleReaction("👍", AN_EVENT_ID))
|
initialState.eventSink.invoke(MessagesEvents.ToggleReaction("👍", A_UNIQUE_ID))
|
||||||
initialState.eventSink.invoke(MessagesEvents.ToggleReaction("👍", AN_EVENT_ID))
|
initialState.eventSink.invoke(MessagesEvents.ToggleReaction("👍", A_UNIQUE_ID))
|
||||||
assert(toggleReactionSuccess)
|
assert(toggleReactionSuccess)
|
||||||
.isCalledExactly(2)
|
.isCalledExactly(2)
|
||||||
.withSequence(
|
.withSequence(
|
||||||
listOf(value("👍"), value(AN_EVENT_ID)),
|
listOf(value("👍"), value(A_UNIQUE_ID)),
|
||||||
listOf(value("👍"), value(AN_EVENT_ID)),
|
listOf(value("👍"), value(A_UNIQUE_ID)),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -361,7 +361,7 @@ class MessagesViewTest {
|
||||||
state = state,
|
state = state,
|
||||||
)
|
)
|
||||||
rule.onAllNodesWithText("👍️").onFirst().performClick()
|
rule.onAllNodesWithText("👍️").onFirst().performClick()
|
||||||
eventsRecorder.assertSingle(MessagesEvents.ToggleReaction("👍️", timelineItem.eventId!!))
|
eventsRecorder.assertSingle(MessagesEvents.ToggleReaction("👍️", timelineItem.id))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -463,7 +463,7 @@ class MessagesViewTest {
|
||||||
// Give time for the close animation to complete
|
// Give time for the close animation to complete
|
||||||
rule.mainClock.advanceTimeBy(milliseconds = 1_000)
|
rule.mainClock.advanceTimeBy(milliseconds = 1_000)
|
||||||
customReactionStateEventsRecorder.assertSingle(CustomReactionEvents.DismissCustomReactionSheet)
|
customReactionStateEventsRecorder.assertSingle(CustomReactionEvents.DismissCustomReactionSheet)
|
||||||
eventsRecorder.assertSingle(MessagesEvents.ToggleReaction(aUnicode, timelineItem.eventId!!))
|
eventsRecorder.assertSingle(MessagesEvents.ToggleReaction(aUnicode, timelineItem.id))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
||||||
|
|
@ -757,7 +757,7 @@ class ActionListPresenterTest {
|
||||||
assertThat(successState.target).isEqualTo(
|
assertThat(successState.target).isEqualTo(
|
||||||
ActionListState.Target.Success(
|
ActionListState.Target.Success(
|
||||||
event = messageEvent,
|
event = messageEvent,
|
||||||
displayEmojiReactions = false,
|
displayEmojiReactions = true,
|
||||||
actions = persistentListOf(
|
actions = persistentListOf(
|
||||||
TimelineItemAction.Edit,
|
TimelineItemAction.Edit,
|
||||||
TimelineItemAction.Copy,
|
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.designsystem.components.avatar.AvatarSize
|
||||||
import io.element.android.libraries.matrix.api.core.EventId
|
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.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.TimelineItemDebugInfo
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState
|
import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.MessageShield
|
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),
|
sendState: LocalEventSendState = LocalEventSendState.Sent(AN_EVENT_ID),
|
||||||
messageShield: MessageShield? = null,
|
messageShield: MessageShield? = null,
|
||||||
) = TimelineItem.Event(
|
) = TimelineItem.Event(
|
||||||
id = eventId?.value.orEmpty(),
|
id = UniqueId(eventId?.value.orEmpty()),
|
||||||
eventId = eventId,
|
eventId = eventId,
|
||||||
transactionId = transactionId,
|
transactionId = transactionId,
|
||||||
senderId = A_USER_ID,
|
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.api.timeline.MatrixTimelineItem
|
||||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
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.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.FakeMatrixRoom
|
||||||
import io.element.android.libraries.matrix.test.room.aRoomInfo
|
import io.element.android.libraries.matrix.test.room.aRoomInfo
|
||||||
import io.element.android.libraries.matrix.test.timeline.FakeTimeline
|
import io.element.android.libraries.matrix.test.timeline.FakeTimeline
|
||||||
|
|
@ -80,7 +82,7 @@ class PinnedMessagesBannerPresenterTest {
|
||||||
timelineItems = flowOf(
|
timelineItems = flowOf(
|
||||||
listOf(
|
listOf(
|
||||||
MatrixTimelineItem.Event(
|
MatrixTimelineItem.Event(
|
||||||
uniqueId = "FAKE_UNIQUE_ID",
|
uniqueId = A_UNIQUE_ID,
|
||||||
event = anEventTimelineItem(
|
event = anEventTimelineItem(
|
||||||
eventId = AN_EVENT_ID,
|
eventId = AN_EVENT_ID,
|
||||||
content = messageContent,
|
content = messageContent,
|
||||||
|
|
@ -112,14 +114,14 @@ class PinnedMessagesBannerPresenterTest {
|
||||||
timelineItems = flowOf(
|
timelineItems = flowOf(
|
||||||
listOf(
|
listOf(
|
||||||
MatrixTimelineItem.Event(
|
MatrixTimelineItem.Event(
|
||||||
uniqueId = "FAKE_UNIQUE_ID",
|
uniqueId = A_UNIQUE_ID,
|
||||||
event = anEventTimelineItem(
|
event = anEventTimelineItem(
|
||||||
eventId = AN_EVENT_ID,
|
eventId = AN_EVENT_ID,
|
||||||
content = messageContent1,
|
content = messageContent1,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
MatrixTimelineItem.Event(
|
MatrixTimelineItem.Event(
|
||||||
uniqueId = "FAKE_UNIQUE_ID_2",
|
uniqueId = A_UNIQUE_ID_2,
|
||||||
event = anEventTimelineItem(
|
event = anEventTimelineItem(
|
||||||
eventId = AN_EVENT_ID_2,
|
eventId = AN_EVENT_ID_2,
|
||||||
content = messageContent2,
|
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.TimelineItem
|
||||||
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemReadMarkerModel
|
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.EventId
|
||||||
|
import io.element.android.libraries.matrix.api.core.UniqueId
|
||||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
|
|
@ -34,7 +35,7 @@ class TimelineItemIndexerTest {
|
||||||
groupedEvents.events.forEach { eventIds.add(it.eventId!!) }
|
groupedEvents.events.forEach { eventIds.add(it.eventId!!) }
|
||||||
},
|
},
|
||||||
TimelineItem.Virtual(
|
TimelineItem.Virtual(
|
||||||
id = "dummy",
|
id = UniqueId("dummy"),
|
||||||
model = TimelineItemReadMarkerModel
|
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.FakeEndPollAction
|
||||||
import io.element.android.features.poll.test.actions.FakeSendPollResponseAction
|
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.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.room.MatrixRoomMembersState
|
||||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
|
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
|
||||||
import io.element.android.libraries.matrix.api.timeline.ReceiptType
|
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.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
|
||||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID_2
|
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.A_USER_ID
|
||||||
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
||||||
import io.element.android.libraries.matrix.test.room.aRoomMember
|
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
|
||||||
import kotlin.time.Duration.Companion.seconds
|
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 {
|
@OptIn(ExperimentalCoroutinesApi::class) class TimelinePresenterTest {
|
||||||
@get:Rule
|
@get:Rule
|
||||||
val warmUpRule = WarmUpRule()
|
val warmUpRule = WarmUpRule()
|
||||||
|
|
@ -131,7 +131,7 @@ private const val FAKE_UNIQUE_ID_2 = "FAKE_UNIQUE_ID_2"
|
||||||
val timeline = FakeTimeline(
|
val timeline = FakeTimeline(
|
||||||
timelineItems = flowOf(
|
timelineItems = flowOf(
|
||||||
listOf(
|
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(
|
val timeline = FakeTimeline(
|
||||||
timelineItems = flowOf(
|
timelineItems = flowOf(
|
||||||
listOf(
|
listOf(
|
||||||
MatrixTimelineItem.Event(FAKE_UNIQUE_ID, anEventTimelineItem()),
|
MatrixTimelineItem.Event(A_UNIQUE_ID, anEventTimelineItem()),
|
||||||
MatrixTimelineItem.Event(
|
MatrixTimelineItem.Event(
|
||||||
uniqueId = FAKE_UNIQUE_ID_2,
|
uniqueId = A_UNIQUE_ID_2,
|
||||||
event = anEventTimelineItem(
|
event = anEventTimelineItem(
|
||||||
eventId = AN_EVENT_ID_2,
|
eventId = AN_EVENT_ID_2,
|
||||||
content = aMessageContent("Test message")
|
content = aMessageContent("Test message")
|
||||||
|
|
@ -201,9 +201,9 @@ private const val FAKE_UNIQUE_ID_2 = "FAKE_UNIQUE_ID_2"
|
||||||
val timeline = FakeTimeline(
|
val timeline = FakeTimeline(
|
||||||
timelineItems = flowOf(
|
timelineItems = flowOf(
|
||||||
listOf(
|
listOf(
|
||||||
MatrixTimelineItem.Event(FAKE_UNIQUE_ID, anEventTimelineItem()),
|
MatrixTimelineItem.Event(A_UNIQUE_ID, anEventTimelineItem()),
|
||||||
MatrixTimelineItem.Event(
|
MatrixTimelineItem.Event(
|
||||||
uniqueId = FAKE_UNIQUE_ID_2,
|
uniqueId = A_UNIQUE_ID_2,
|
||||||
event = anEventTimelineItem(
|
event = anEventTimelineItem(
|
||||||
eventId = AN_EVENT_ID_2,
|
eventId = AN_EVENT_ID_2,
|
||||||
content = aMessageContent("Test message")
|
content = aMessageContent("Test message")
|
||||||
|
|
@ -243,9 +243,9 @@ private const val FAKE_UNIQUE_ID_2 = "FAKE_UNIQUE_ID_2"
|
||||||
val timeline = FakeTimeline(
|
val timeline = FakeTimeline(
|
||||||
timelineItems = flowOf(
|
timelineItems = flowOf(
|
||||||
listOf(
|
listOf(
|
||||||
MatrixTimelineItem.Event(FAKE_UNIQUE_ID, anEventTimelineItem()),
|
MatrixTimelineItem.Event(A_UNIQUE_ID, anEventTimelineItem()),
|
||||||
MatrixTimelineItem.Event(
|
MatrixTimelineItem.Event(
|
||||||
uniqueId = FAKE_UNIQUE_ID_2,
|
uniqueId = A_UNIQUE_ID_2,
|
||||||
event = anEventTimelineItem(
|
event = anEventTimelineItem(
|
||||||
eventId = AN_EVENT_ID_2,
|
eventId = AN_EVENT_ID_2,
|
||||||
content = aMessageContent("Test message")
|
content = aMessageContent("Test message")
|
||||||
|
|
@ -279,8 +279,8 @@ private const val FAKE_UNIQUE_ID_2 = "FAKE_UNIQUE_ID_2"
|
||||||
val timeline = FakeTimeline(
|
val timeline = FakeTimeline(
|
||||||
timelineItems = flowOf(
|
timelineItems = flowOf(
|
||||||
listOf(
|
listOf(
|
||||||
MatrixTimelineItem.Virtual(FAKE_UNIQUE_ID, VirtualTimelineItem.ReadMarker),
|
MatrixTimelineItem.Virtual(A_UNIQUE_ID, VirtualTimelineItem.ReadMarker),
|
||||||
MatrixTimelineItem.Virtual(FAKE_UNIQUE_ID, VirtualTimelineItem.ReadMarker)
|
MatrixTimelineItem.Virtual(A_UNIQUE_ID, VirtualTimelineItem.ReadMarker)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
).apply {
|
).apply {
|
||||||
|
|
@ -310,13 +310,13 @@ private const val FAKE_UNIQUE_ID_2 = "FAKE_UNIQUE_ID_2"
|
||||||
assertThat(initialState.newEventState).isEqualTo(NewEventState.None)
|
assertThat(initialState.newEventState).isEqualTo(NewEventState.None)
|
||||||
assertThat(initialState.timelineItems.size).isEqualTo(0)
|
assertThat(initialState.timelineItems.size).isEqualTo(0)
|
||||||
timelineItems.emit(
|
timelineItems.emit(
|
||||||
listOf(MatrixTimelineItem.Event("0", anEventTimelineItem(content = aMessageContent())))
|
listOf(MatrixTimelineItem.Event(UniqueId("0"), anEventTimelineItem(content = aMessageContent())))
|
||||||
)
|
)
|
||||||
consumeItemsUntilPredicate { it.timelineItems.size == 1 }
|
consumeItemsUntilPredicate { it.timelineItems.size == 1 }
|
||||||
// Mimics sending a message, and assert newEventState is FromMe
|
// Mimics sending a message, and assert newEventState is FromMe
|
||||||
timelineItems.getAndUpdate { items ->
|
timelineItems.getAndUpdate { items ->
|
||||||
val event = anEventTimelineItem(content = aMessageContent(), isOwn = true)
|
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 }
|
consumeItemsUntilPredicate { it.timelineItems.size == 2 }
|
||||||
awaitLastSequentialItem().also { state ->
|
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
|
// Mimics receiving a message without clearing the previous FromMe
|
||||||
timelineItems.getAndUpdate { items ->
|
timelineItems.getAndUpdate { items ->
|
||||||
val event = anEventTimelineItem(content = aMessageContent())
|
val event = anEventTimelineItem(content = aMessageContent())
|
||||||
items + listOf(MatrixTimelineItem.Event("2", event))
|
items + listOf(MatrixTimelineItem.Event(UniqueId("2"), event))
|
||||||
}
|
}
|
||||||
consumeItemsUntilPredicate { it.timelineItems.size == 3 }
|
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
|
// Mimics receiving a message and assert newEventState is FromOther
|
||||||
timelineItems.getAndUpdate { items ->
|
timelineItems.getAndUpdate { items ->
|
||||||
val event = anEventTimelineItem(content = aMessageContent())
|
val event = anEventTimelineItem(content = aMessageContent())
|
||||||
items + listOf(MatrixTimelineItem.Event("3", event))
|
items + listOf(MatrixTimelineItem.Event(UniqueId("3"), event))
|
||||||
}
|
}
|
||||||
consumeItemsUntilPredicate { it.timelineItems.size == 4 }
|
consumeItemsUntilPredicate { it.timelineItems.size == 4 }
|
||||||
awaitLastSequentialItem().also { state ->
|
awaitLastSequentialItem().also { state ->
|
||||||
|
|
@ -381,7 +381,7 @@ private const val FAKE_UNIQUE_ID_2 = "FAKE_UNIQUE_ID_2"
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
timelineItems.emit(
|
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()
|
val item = awaitItem().timelineItems.first()
|
||||||
assertThat(item).isInstanceOf(TimelineItem.Event::class.java)
|
assertThat(item).isInstanceOf(TimelineItem.Event::class.java)
|
||||||
|
|
@ -476,7 +476,7 @@ private const val FAKE_UNIQUE_ID_2 = "FAKE_UNIQUE_ID_2"
|
||||||
timelineItems = flowOf(
|
timelineItems = flowOf(
|
||||||
listOf(
|
listOf(
|
||||||
MatrixTimelineItem.Event(
|
MatrixTimelineItem.Event(
|
||||||
uniqueId = FAKE_UNIQUE_ID,
|
uniqueId = A_UNIQUE_ID,
|
||||||
event = anEventTimelineItem(),
|
event = anEventTimelineItem(),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
@ -532,7 +532,7 @@ private const val FAKE_UNIQUE_ID_2 = "FAKE_UNIQUE_ID_2"
|
||||||
timelineItems = flowOf(
|
timelineItems = flowOf(
|
||||||
listOf(
|
listOf(
|
||||||
MatrixTimelineItem.Event(
|
MatrixTimelineItem.Event(
|
||||||
uniqueId = FAKE_UNIQUE_ID,
|
uniqueId = A_UNIQUE_ID,
|
||||||
event = anEventTimelineItem(eventId = AN_EVENT_ID),
|
event = anEventTimelineItem(eventId = AN_EVENT_ID),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
@ -618,7 +618,7 @@ private const val FAKE_UNIQUE_ID_2 = "FAKE_UNIQUE_ID_2"
|
||||||
timelineItems = flowOf(
|
timelineItems = flowOf(
|
||||||
listOf(
|
listOf(
|
||||||
MatrixTimelineItem.Event(
|
MatrixTimelineItem.Event(
|
||||||
FAKE_UNIQUE_ID,
|
A_UNIQUE_ID,
|
||||||
anEventTimelineItem(
|
anEventTimelineItem(
|
||||||
sender = A_USER_ID,
|
sender = A_USER_ID,
|
||||||
receipts = persistentListOf(
|
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.timeline.model.virtual.TimelineItemLoadingIndicatorModel
|
||||||
import io.element.android.features.messages.impl.typing.TypingNotificationState
|
import io.element.android.features.messages.impl.typing.TypingNotificationState
|
||||||
import io.element.android.features.messages.impl.typing.aTypingNotificationState
|
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.core.UserId
|
||||||
import io.element.android.libraries.matrix.api.timeline.Timeline
|
import io.element.android.libraries.matrix.api.timeline.Timeline
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.MessageShield
|
import io.element.android.libraries.matrix.api.timeline.item.event.MessageShield
|
||||||
|
|
@ -54,7 +55,7 @@ class TimelineViewTest {
|
||||||
state = aTimelineState(
|
state = aTimelineState(
|
||||||
timelineItems = persistentListOf<TimelineItem>(
|
timelineItems = persistentListOf<TimelineItem>(
|
||||||
TimelineItem.Virtual(
|
TimelineItem.Virtual(
|
||||||
id = "backward_pagination",
|
id = UniqueId("backward_pagination"),
|
||||||
model = TimelineItemLoadingIndicatorModel(Timeline.PaginationDirection.BACKWARDS, 0)
|
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.event.TimelineItemStateEventContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.virtual.aTimelineItemDaySeparatorModel
|
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.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.api.timeline.item.event.LocalEventSendState
|
||||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||||
import io.element.android.libraries.matrix.test.A_USER_ID
|
import io.element.android.libraries.matrix.test.A_USER_ID
|
||||||
|
|
@ -37,7 +38,7 @@ class TimelineItemGrouperTest {
|
||||||
private val sut = TimelineItemGrouper()
|
private val sut = TimelineItemGrouper()
|
||||||
|
|
||||||
private val aGroupableItem = TimelineItem.Event(
|
private val aGroupableItem = TimelineItem.Event(
|
||||||
id = "0",
|
id = UniqueId("0"),
|
||||||
senderId = A_USER_ID,
|
senderId = A_USER_ID,
|
||||||
senderAvatar = anAvatarData(),
|
senderAvatar = anAvatarData(),
|
||||||
senderProfile = aProfileTimelineDetailsReady(displayName = ""),
|
senderProfile = aProfileTimelineDetailsReady(displayName = ""),
|
||||||
|
|
@ -54,7 +55,7 @@ class TimelineItemGrouperTest {
|
||||||
messageShield = null,
|
messageShield = null,
|
||||||
)
|
)
|
||||||
private val aNonGroupableItem = aMessageEvent()
|
private val aNonGroupableItem = aMessageEvent()
|
||||||
private val aNonGroupableItemNoEvent = TimelineItem.Virtual("virtual", aTimelineItemDaySeparatorModel("Today"))
|
private val aNonGroupableItemNoEvent = TimelineItem.Virtual(UniqueId("virtual"), aTimelineItemDaySeparatorModel("Today"))
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `test empty`() {
|
fun `test empty`() {
|
||||||
|
|
@ -82,8 +83,8 @@ class TimelineItemGrouperTest {
|
||||||
fun `test groupables and ensure reordering`() {
|
fun `test groupables and ensure reordering`() {
|
||||||
val result = sut.group(
|
val result = sut.group(
|
||||||
listOf(
|
listOf(
|
||||||
aGroupableItem.copy(id = "1"),
|
aGroupableItem.copy(id = UniqueId("1")),
|
||||||
aGroupableItem.copy(id = "0"),
|
aGroupableItem.copy(id = UniqueId("0")),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
assertThat(result).isEqualTo(
|
assertThat(result).isEqualTo(
|
||||||
|
|
@ -91,8 +92,8 @@ class TimelineItemGrouperTest {
|
||||||
TimelineItem.GroupedEvents(
|
TimelineItem.GroupedEvents(
|
||||||
id = computeGroupIdWith(aGroupableItem),
|
id = computeGroupIdWith(aGroupableItem),
|
||||||
events = listOf(
|
events = listOf(
|
||||||
aGroupableItem.copy("0"),
|
aGroupableItem.copy(id = UniqueId("0")),
|
||||||
aGroupableItem.copy(id = "1"),
|
aGroupableItem.copy(id = UniqueId("1")),
|
||||||
).toImmutableList(),
|
).toImmutableList(),
|
||||||
aggregatedReadReceipts = emptyList<ReadReceiptData>().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`() {
|
fun `when calling multiple time the method group over a growing list of groupable items, then groupId is stable`() {
|
||||||
// When
|
// When
|
||||||
val groupableItems = mutableListOf(
|
val groupableItems = mutableListOf(
|
||||||
aGroupableItem.copy(id = "1"),
|
aGroupableItem.copy(id = UniqueId("1")),
|
||||||
aGroupableItem.copy(id = "2")
|
aGroupableItem.copy(id = UniqueId("2"))
|
||||||
)
|
)
|
||||||
val expectedGroupId = sut.group(groupableItems).first().identifier()
|
val expectedGroupId = sut.group(groupableItems).first().identifier()
|
||||||
groupableItems.add(0, aGroupableItem.copy("3"))
|
groupableItems.add(0, aGroupableItem.copy(UniqueId("3")))
|
||||||
groupableItems.add(2, aGroupableItem.copy("4"))
|
groupableItems.add(2, aGroupableItem.copy(UniqueId("4")))
|
||||||
groupableItems.add(aGroupableItem.copy("5"))
|
groupableItems.add(aGroupableItem.copy(UniqueId("5")))
|
||||||
val actualGroupId = sut.group(groupableItems).first().identifier()
|
val actualGroupId = sut.group(groupableItems).first().identifier()
|
||||||
// Then
|
// Then
|
||||||
assertThat(actualGroupId).isEqualTo(expectedGroupId)
|
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 com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.libraries.core.mimetype.MimeTypes
|
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.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.MatrixTimelineItem
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo
|
import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem
|
import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem
|
||||||
|
|
@ -80,7 +81,7 @@ fun TestScope.aDefaultRedactedVoiceMessageManager(
|
||||||
|
|
||||||
fun aRedactedMatrixTimeline(eventId: EventId) = listOf<MatrixTimelineItem>(
|
fun aRedactedMatrixTimeline(eventId: EventId) = listOf<MatrixTimelineItem>(
|
||||||
MatrixTimelineItem.Event(
|
MatrixTimelineItem.Event(
|
||||||
uniqueId = "0",
|
uniqueId = UniqueId("0"),
|
||||||
event = EventTimelineItem(
|
event = EventTimelineItem(
|
||||||
eventId = eventId,
|
eventId = eventId,
|
||||||
transactionId = null,
|
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
|
package io.element.android.features.poll.impl
|
||||||
|
|
||||||
import io.element.android.libraries.matrix.api.core.EventId
|
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.poll.PollAnswer
|
||||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
|
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.PollContent
|
import io.element.android.libraries.matrix.api.timeline.item.event.PollContent
|
||||||
|
|
@ -32,8 +33,8 @@ fun aPollTimelineItems(
|
||||||
return flowOf(
|
return flowOf(
|
||||||
polls.map { entry ->
|
polls.map { entry ->
|
||||||
MatrixTimelineItem.Event(
|
MatrixTimelineItem.Event(
|
||||||
entry.key.value,
|
uniqueId = UniqueId(entry.key.value),
|
||||||
anEventTimelineItem(
|
event = anEventTimelineItem(
|
||||||
eventId = entry.key,
|
eventId = entry.key,
|
||||||
content = entry.value,
|
content = entry.value,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,7 @@ dependencies {
|
||||||
implementation(projects.features.lockscreen.api)
|
implementation(projects.features.lockscreen.api)
|
||||||
implementation(projects.features.analytics.api)
|
implementation(projects.features.analytics.api)
|
||||||
implementation(projects.features.ftue.api)
|
implementation(projects.features.ftue.api)
|
||||||
|
implementation(projects.features.licenses.api)
|
||||||
implementation(projects.features.logout.api)
|
implementation(projects.features.logout.api)
|
||||||
implementation(projects.features.roomlist.api)
|
implementation(projects.features.roomlist.api)
|
||||||
implementation(projects.services.analytics.api)
|
implementation(projects.services.analytics.api)
|
||||||
|
|
@ -91,7 +92,6 @@ dependencies {
|
||||||
testImplementation(projects.features.rageshake.test)
|
testImplementation(projects.features.rageshake.test)
|
||||||
testImplementation(projects.features.rageshake.impl)
|
testImplementation(projects.features.rageshake.impl)
|
||||||
testImplementation(projects.features.logout.test)
|
testImplementation(projects.features.logout.test)
|
||||||
testImplementation(projects.features.roomlist.test)
|
|
||||||
testImplementation(projects.libraries.indicator.impl)
|
testImplementation(projects.libraries.indicator.impl)
|
||||||
testImplementation(projects.libraries.pushproviders.test)
|
testImplementation(projects.libraries.pushproviders.test)
|
||||||
testImplementation(projects.libraries.fullscreenintent.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.Assisted
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import io.element.android.anvilannotations.ContributesNode
|
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.lockscreen.api.LockScreenEntryPoint
|
||||||
import io.element.android.features.logout.api.LogoutEntryPoint
|
import io.element.android.features.logout.api.LogoutEntryPoint
|
||||||
import io.element.android.features.preferences.api.PreferencesEntryPoint
|
import io.element.android.features.preferences.api.PreferencesEntryPoint
|
||||||
|
|
@ -59,6 +60,7 @@ class PreferencesFlowNode @AssistedInject constructor(
|
||||||
private val lockScreenEntryPoint: LockScreenEntryPoint,
|
private val lockScreenEntryPoint: LockScreenEntryPoint,
|
||||||
private val notificationTroubleShootEntryPoint: NotificationTroubleShootEntryPoint,
|
private val notificationTroubleShootEntryPoint: NotificationTroubleShootEntryPoint,
|
||||||
private val logoutEntryPoint: LogoutEntryPoint,
|
private val logoutEntryPoint: LogoutEntryPoint,
|
||||||
|
private val openSourceLicensesEntryPoint: OpenSourceLicensesEntryPoint,
|
||||||
) : BaseFlowNode<PreferencesFlowNode.NavTarget>(
|
) : BaseFlowNode<PreferencesFlowNode.NavTarget>(
|
||||||
backstack = BackStack(
|
backstack = BackStack(
|
||||||
initialElement = plugins.filterIsInstance<PreferencesEntryPoint.Params>().first().initialElement.toNavTarget(),
|
initialElement = plugins.filterIsInstance<PreferencesEntryPoint.Params>().first().initialElement.toNavTarget(),
|
||||||
|
|
@ -106,6 +108,9 @@ class PreferencesFlowNode @AssistedInject constructor(
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data object SignOut : NavTarget
|
data object SignOut : NavTarget
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data object OssLicenses : NavTarget
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||||
|
|
@ -170,7 +175,12 @@ class PreferencesFlowNode @AssistedInject constructor(
|
||||||
createNode<ConfigureTracingNode>(buildContext)
|
createNode<ConfigureTracingNode>(buildContext)
|
||||||
}
|
}
|
||||||
NavTarget.About -> {
|
NavTarget.About -> {
|
||||||
createNode<AboutNode>(buildContext)
|
val callback = object : AboutNode.Callback {
|
||||||
|
override fun openOssLicenses() {
|
||||||
|
backstack.push(NavTarget.OssLicenses)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
createNode<AboutNode>(buildContext, listOf(callback))
|
||||||
}
|
}
|
||||||
NavTarget.AnalyticsSettings -> {
|
NavTarget.AnalyticsSettings -> {
|
||||||
createNode<AnalyticsSettingsNode>(buildContext)
|
createNode<AnalyticsSettingsNode>(buildContext)
|
||||||
|
|
@ -232,6 +242,9 @@ class PreferencesFlowNode @AssistedInject constructor(
|
||||||
.callback(callBack)
|
.callback(callBack)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
is NavTarget.OssLicenses -> {
|
||||||
|
openSourceLicensesEntryPoint.getNode(this, buildContext)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,6 @@ import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import io.element.android.anvilannotations.ContributesNode
|
import io.element.android.anvilannotations.ContributesNode
|
||||||
import io.element.android.compound.theme.ElementTheme
|
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.androidutils.browser.openUrlInChromeCustomTab
|
||||||
import io.element.android.libraries.di.SessionScope
|
import io.element.android.libraries.di.SessionScope
|
||||||
|
|
||||||
|
|
@ -36,8 +35,11 @@ class AboutNode @AssistedInject constructor(
|
||||||
@Assisted buildContext: BuildContext,
|
@Assisted buildContext: BuildContext,
|
||||||
@Assisted plugins: List<Plugin>,
|
@Assisted plugins: List<Plugin>,
|
||||||
private val presenter: AboutPresenter,
|
private val presenter: AboutPresenter,
|
||||||
private val openSourceLicensesProvider: OpenSourceLicensesProvider,
|
|
||||||
) : Node(buildContext, plugins = plugins) {
|
) : Node(buildContext, plugins = plugins) {
|
||||||
|
interface Callback : Plugin {
|
||||||
|
fun openOssLicenses()
|
||||||
|
}
|
||||||
|
|
||||||
private fun onElementLegalClick(
|
private fun onElementLegalClick(
|
||||||
activity: Activity,
|
activity: Activity,
|
||||||
darkTheme: Boolean,
|
darkTheme: Boolean,
|
||||||
|
|
@ -58,7 +60,7 @@ class AboutNode @AssistedInject constructor(
|
||||||
onElementLegalClick(activity, isDark, elementLegal)
|
onElementLegalClick(activity, isDark, elementLegal)
|
||||||
},
|
},
|
||||||
onOpenSourceLicensesClick = {
|
onOpenSourceLicensesClick = {
|
||||||
openSourceLicensesProvider.navigateToOpenSourceLicenses(activity)
|
plugins.filterIsInstance<Callback>().forEach { it.openOssLicenses() }
|
||||||
},
|
},
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -17,18 +17,14 @@
|
||||||
package io.element.android.features.preferences.impl.about
|
package io.element.android.features.preferences.impl.about
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import io.element.android.features.preferences.api.OpenSourceLicensesProvider
|
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class AboutPresenter @Inject constructor(
|
class AboutPresenter @Inject constructor() : Presenter<AboutState> {
|
||||||
private val openSourceLicensesProvider: OpenSourceLicensesProvider,
|
|
||||||
) : Presenter<AboutState> {
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): AboutState {
|
override fun present(): AboutState {
|
||||||
return AboutState(
|
return AboutState(
|
||||||
elementLegals = getAllLegals(),
|
elementLegals = getAllLegals(),
|
||||||
hasOpenSourcesLicenses = openSourceLicensesProvider.hasOpenSourceLicenses,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,5 +18,4 @@ package io.element.android.features.preferences.impl.about
|
||||||
|
|
||||||
data class AboutState(
|
data class AboutState(
|
||||||
val elementLegals: List<ElementLegal>,
|
val elementLegals: List<ElementLegal>,
|
||||||
val hasOpenSourcesLicenses: Boolean,
|
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -22,13 +22,11 @@ open class AboutStateProvider : PreviewParameterProvider<AboutState> {
|
||||||
override val values: Sequence<AboutState>
|
override val values: Sequence<AboutState>
|
||||||
get() = sequenceOf(
|
get() = sequenceOf(
|
||||||
anAboutState(),
|
anAboutState(),
|
||||||
anAboutState(hasOpenSourcesLicenses = true),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun anAboutState(
|
fun anAboutState(
|
||||||
hasOpenSourcesLicenses: Boolean = false,
|
elementLegals: List<ElementLegal> = getAllLegals(),
|
||||||
) = AboutState(
|
) = AboutState(
|
||||||
elementLegals = getAllLegals(),
|
elementLegals = elementLegals,
|
||||||
hasOpenSourcesLicenses = hasOpenSourcesLicenses,
|
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -45,12 +45,10 @@ fun AboutView(
|
||||||
onClick = { onElementLegalClick(elementLegal) }
|
onClick = { onElementLegalClick(elementLegal) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (state.hasOpenSourcesLicenses) {
|
PreferenceText(
|
||||||
PreferenceText(
|
title = stringResource(id = CommonStrings.common_open_source_licenses),
|
||||||
title = stringResource(id = CommonStrings.common_open_source_licenses),
|
onClick = onOpenSourceLicensesClick,
|
||||||
onClick = onOpenSourceLicensesClick,
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,6 @@ import coil.annotation.ExperimentalCoilApi
|
||||||
import com.squareup.anvil.annotations.ContributesBinding
|
import com.squareup.anvil.annotations.ContributesBinding
|
||||||
import io.element.android.features.ftue.api.state.FtueService
|
import io.element.android.features.ftue.api.state.FtueService
|
||||||
import io.element.android.features.preferences.impl.DefaultCacheService
|
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.core.coroutine.CoroutineDispatchers
|
||||||
import io.element.android.libraries.di.ApplicationContext
|
import io.element.android.libraries.di.ApplicationContext
|
||||||
import io.element.android.libraries.di.SessionScope
|
import io.element.android.libraries.di.SessionScope
|
||||||
|
|
@ -47,7 +46,6 @@ class DefaultClearCacheUseCase @Inject constructor(
|
||||||
private val defaultCacheService: DefaultCacheService,
|
private val defaultCacheService: DefaultCacheService,
|
||||||
private val okHttpClient: Provider<OkHttpClient>,
|
private val okHttpClient: Provider<OkHttpClient>,
|
||||||
private val ftueService: FtueService,
|
private val ftueService: FtueService,
|
||||||
private val migrationScreenStore: MigrationScreenStore,
|
|
||||||
private val pushService: PushService,
|
private val pushService: PushService,
|
||||||
) : ClearCacheUseCase {
|
) : ClearCacheUseCase {
|
||||||
override suspend fun invoke() = withContext(coroutineDispatchers.io) {
|
override suspend fun invoke() = withContext(coroutineDispatchers.io) {
|
||||||
|
|
@ -64,8 +62,6 @@ class DefaultClearCacheUseCase @Inject constructor(
|
||||||
context.cacheDir.deleteRecursively()
|
context.cacheDir.deleteRecursively()
|
||||||
// Clear some settings
|
// Clear some settings
|
||||||
ftueService.reset()
|
ftueService.reset()
|
||||||
// Clear migration screen store
|
|
||||||
migrationScreenStore.reset()
|
|
||||||
// Ensure any error will be displayed again
|
// Ensure any error will be displayed again
|
||||||
pushService.setIgnoreRegistrationError(matrixClient.sessionId, false)
|
pushService.setIgnoreRegistrationError(matrixClient.sessionId, false)
|
||||||
// Ensure the app is restarted
|
// 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_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_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_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_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_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>
|
<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
|
@Test
|
||||||
fun `present - initial state`() = runTest {
|
fun `present - initial state`() = runTest {
|
||||||
val presenter = AboutPresenter(FakeOpenSourceLicensesProvider(hasOpenSourceLicenses = true))
|
val presenter = AboutPresenter()
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
assertThat(initialState.elementLegals).isEqualTo(getAllLegals())
|
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.activity.ComponentActivity
|
||||||
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
|
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
|
||||||
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||||
import androidx.compose.ui.test.onNodeWithText
|
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
import io.element.android.tests.testutils.EnsureNeverCalled
|
import io.element.android.tests.testutils.EnsureNeverCalled
|
||||||
|
|
@ -61,21 +60,10 @@ class AboutViewTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `if open source licenses are not available, the entry is not displayed`() {
|
fun `clicking on the open source licenses invokes the expected callback`() {
|
||||||
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`() {
|
|
||||||
ensureCalledOnce { callback ->
|
ensureCalledOnce { callback ->
|
||||||
rule.setAboutView(
|
rule.setAboutView(
|
||||||
anAboutState(
|
anAboutState(),
|
||||||
hasOpenSourcesLicenses = true,
|
|
||||||
),
|
|
||||||
onOpenSourceLicensesClick = callback,
|
onOpenSourceLicensesClick = callback,
|
||||||
)
|
)
|
||||||
rule.clickOn(CommonStrings.common_open_source_licenses)
|
rule.clickOn(CommonStrings.common_open_source_licenses)
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,6 @@ import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.features.ftue.test.FakeFtueService
|
import io.element.android.features.ftue.test.FakeFtueService
|
||||||
import io.element.android.features.preferences.impl.DefaultCacheService
|
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.api.core.SessionId
|
||||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||||
import io.element.android.libraries.push.test.FakePushService
|
import io.element.android.libraries.push.test.FakePushService
|
||||||
|
|
@ -47,10 +46,6 @@ class DefaultClearCacheUseCaseTest {
|
||||||
val ftueService = FakeFtueService(
|
val ftueService = FakeFtueService(
|
||||||
resetLambda = resetFtueLambda,
|
resetLambda = resetFtueLambda,
|
||||||
)
|
)
|
||||||
val resetMigrationLambda = lambdaRecorder<Unit> { }
|
|
||||||
val migrationScreenStore = InMemoryMigrationScreenStore(
|
|
||||||
resetLambda = resetMigrationLambda,
|
|
||||||
)
|
|
||||||
val setIgnoreRegistrationErrorLambda = lambdaRecorder<SessionId, Boolean, Unit> { _, _ -> }
|
val setIgnoreRegistrationErrorLambda = lambdaRecorder<SessionId, Boolean, Unit> { _, _ -> }
|
||||||
val pushService = FakePushService(
|
val pushService = FakePushService(
|
||||||
setIgnoreRegistrationErrorLambda = setIgnoreRegistrationErrorLambda
|
setIgnoreRegistrationErrorLambda = setIgnoreRegistrationErrorLambda
|
||||||
|
|
@ -62,14 +57,12 @@ class DefaultClearCacheUseCaseTest {
|
||||||
defaultCacheService = defaultCacheService,
|
defaultCacheService = defaultCacheService,
|
||||||
okHttpClient = { OkHttpClient.Builder().build() },
|
okHttpClient = { OkHttpClient.Builder().build() },
|
||||||
ftueService = ftueService,
|
ftueService = ftueService,
|
||||||
migrationScreenStore = migrationScreenStore,
|
|
||||||
pushService = pushService,
|
pushService = pushService,
|
||||||
)
|
)
|
||||||
defaultCacheService.clearedCacheEventFlow.test {
|
defaultCacheService.clearedCacheEventFlow.test {
|
||||||
sut.invoke()
|
sut.invoke()
|
||||||
clearCacheLambda.assertions().isCalledOnce()
|
clearCacheLambda.assertions().isCalledOnce()
|
||||||
resetFtueLambda.assertions().isCalledOnce()
|
resetFtueLambda.assertions().isCalledOnce()
|
||||||
resetMigrationLambda.assertions().isCalledOnce()
|
|
||||||
setIgnoreRegistrationErrorLambda.assertions().isCalledOnce()
|
setIgnoreRegistrationErrorLambda.assertions().isCalledOnce()
|
||||||
.with(value(matrixClient.sessionId), value(false))
|
.with(value(matrixClient.sessionId), value(false))
|
||||||
assertThat(awaitItem()).isEqualTo(matrixClient.sessionId)
|
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_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_crash_logs">"Buzilish jurnallarini yuboring"</string>
|
||||||
<string name="screen_bug_report_include_logs">"Jurnallarga ruxsat bering"</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_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>
|
<string name="screen_bug_report_rash_logs_alert_title">"%1$soxirgi marta ishlatilganda qulab tushdi. Biz bilan nosozlik hisobotini baham ko\'rmoqchimisiz?"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -289,6 +289,7 @@ class DefaultBugReporterTest {
|
||||||
slidingSyncProxy = null,
|
slidingSyncProxy = null,
|
||||||
passphrase = null,
|
passphrase = null,
|
||||||
sessionPath = "session",
|
sessionPath = "session",
|
||||||
|
cachePath = "cache",
|
||||||
)
|
)
|
||||||
@Test
|
@Test
|
||||||
fun `test sendBugReport error`() = runTest {
|
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_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_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_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_custom">"Maxsus"</string>
|
||||||
<string name="screen_room_details_notification_mode_default">"Standart"</string>
|
<string name="screen_room_details_notification_mode_default">"Standart"</string>
|
||||||
<string name="screen_room_details_notification_title">"Bildirishnomalar"</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.analytics.test)
|
||||||
testImplementation(projects.services.toolbox.test)
|
testImplementation(projects.services.toolbox.test)
|
||||||
testImplementation(projects.features.networkmonitor.test)
|
testImplementation(projects.features.networkmonitor.test)
|
||||||
testImplementation(projects.features.roomlist.test)
|
|
||||||
testImplementation(projects.tests.testutils)
|
testImplementation(projects.tests.testutils)
|
||||||
testImplementation(projects.features.leaveroom.test)
|
testImplementation(projects.features.leaveroom.test)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,6 @@ open class RoomListContentStateProvider : PreviewParameterProvider<RoomListConte
|
||||||
aRoomsContentState(summaries = persistentListOf()),
|
aRoomsContentState(summaries = persistentListOf()),
|
||||||
aSkeletonContentState(),
|
aSkeletonContentState(),
|
||||||
anEmptyContentState(),
|
anEmptyContentState(),
|
||||||
aMigrationContentState(),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -43,8 +42,6 @@ internal fun aRoomsContentState(
|
||||||
summaries = summaries,
|
summaries = summaries,
|
||||||
)
|
)
|
||||||
|
|
||||||
internal fun aMigrationContentState() = RoomListContentState.Migration
|
|
||||||
|
|
||||||
internal fun aSkeletonContentState() = RoomListContentState.Skeleton(16)
|
internal fun aSkeletonContentState() = RoomListContentState.Skeleton(16)
|
||||||
|
|
||||||
internal fun anEmptyContentState() = RoomListContentState.Empty
|
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.networkmonitor.api.NetworkStatus
|
||||||
import io.element.android.features.roomlist.impl.datasource.RoomListDataSource
|
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.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.model.RoomListRoomSummary
|
||||||
import io.element.android.features.roomlist.impl.search.RoomListSearchEvents
|
import io.element.android.features.roomlist.impl.search.RoomListSearchEvents
|
||||||
import io.element.android.features.roomlist.impl.search.RoomListSearchState
|
import io.element.android.features.roomlist.impl.search.RoomListSearchState
|
||||||
|
|
@ -93,7 +92,6 @@ class RoomListPresenter @Inject constructor(
|
||||||
private val indicatorService: IndicatorService,
|
private val indicatorService: IndicatorService,
|
||||||
private val filtersPresenter: Presenter<RoomListFiltersState>,
|
private val filtersPresenter: Presenter<RoomListFiltersState>,
|
||||||
private val searchPresenter: Presenter<RoomListSearchState>,
|
private val searchPresenter: Presenter<RoomListSearchState>,
|
||||||
private val migrationScreenPresenter: Presenter<MigrationScreenState>,
|
|
||||||
private val sessionPreferencesStore: SessionPreferencesStore,
|
private val sessionPreferencesStore: SessionPreferencesStore,
|
||||||
private val analyticsService: AnalyticsService,
|
private val analyticsService: AnalyticsService,
|
||||||
private val acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState>,
|
private val acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState>,
|
||||||
|
|
@ -210,7 +208,6 @@ class RoomListPresenter @Inject constructor(
|
||||||
roomListDataSource.allRooms.collect { value = AsyncData.Success(it) }
|
roomListDataSource.allRooms.collect { value = AsyncData.Success(it) }
|
||||||
}
|
}
|
||||||
val loadingState by roomListDataSource.loadingState.collectAsState()
|
val loadingState by roomListDataSource.loadingState.collectAsState()
|
||||||
val showMigration = migrationScreenPresenter.present().isMigrating
|
|
||||||
val showEmpty by remember {
|
val showEmpty by remember {
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
(loadingState as? RoomList.LoadingState.Loaded)?.numberOfRooms == 0
|
(loadingState as? RoomList.LoadingState.Loaded)?.numberOfRooms == 0
|
||||||
|
|
@ -222,7 +219,6 @@ class RoomListPresenter @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return when {
|
return when {
|
||||||
showMigration -> RoomListContentState.Migration
|
|
||||||
showEmpty -> RoomListContentState.Empty
|
showEmpty -> RoomListContentState.Empty
|
||||||
showSkeleton -> RoomListContentState.Skeleton(count = 16)
|
showSkeleton -> RoomListContentState.Skeleton(count = 16)
|
||||||
else -> {
|
else -> {
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@ data class RoomListState(
|
||||||
val eventSink: (RoomListEvents) -> Unit,
|
val eventSink: (RoomListEvents) -> Unit,
|
||||||
) {
|
) {
|
||||||
val displayFilters = contentState is RoomListContentState.Rooms
|
val displayFilters = contentState is RoomListContentState.Rooms
|
||||||
val displayActions = contentState !is RoomListContentState.Migration
|
val displayActions = true
|
||||||
|
|
||||||
sealed interface ContextMenu {
|
sealed interface ContextMenu {
|
||||||
data object Hidden : ContextMenu
|
data object Hidden : ContextMenu
|
||||||
|
|
@ -72,7 +72,6 @@ enum class SecurityBannerState {
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
sealed interface RoomListContentState {
|
sealed interface RoomListContentState {
|
||||||
data object Migration : RoomListContentState
|
|
||||||
data class Skeleton(val count: Int) : RoomListContentState
|
data class Skeleton(val count: Int) : RoomListContentState
|
||||||
data object Empty : RoomListContentState
|
data object Empty : RoomListContentState
|
||||||
data class Rooms(
|
data class Rooms(
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,6 @@ open class RoomListStateProvider : PreviewParameterProvider<RoomListState> {
|
||||||
aRoomListState(contentState = aRoomsContentState(securityBannerState = SecurityBannerState.RecoveryKeyConfirmation)),
|
aRoomListState(contentState = aRoomsContentState(securityBannerState = SecurityBannerState.RecoveryKeyConfirmation)),
|
||||||
aRoomListState(contentState = anEmptyContentState()),
|
aRoomListState(contentState = anEmptyContentState()),
|
||||||
aRoomListState(contentState = aSkeletonContentState()),
|
aRoomListState(contentState = aSkeletonContentState()),
|
||||||
aRoomListState(matrixUser = MatrixUser(userId = UserId("@id:domain")), contentState = aMigrationContentState()),
|
|
||||||
aRoomListState(searchState = aRoomListSearchState(isSearchActive = true, query = "Test")),
|
aRoomListState(searchState = aRoomListSearchState(isSearchActive = true, query = "Test")),
|
||||||
aRoomListState(contentState = aRoomsContentState(securityBannerState = SecurityBannerState.SetUpRecovery)),
|
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.RoomListFiltersState
|
||||||
import io.element.android.features.roomlist.impl.filters.aRoomListFiltersState
|
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.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.features.roomlist.impl.model.RoomListRoomSummary
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
|
|
@ -78,9 +77,6 @@ fun RoomListContentView(
|
||||||
) {
|
) {
|
||||||
Box(modifier = modifier) {
|
Box(modifier = modifier) {
|
||||||
when (contentState) {
|
when (contentState) {
|
||||||
is RoomListContentState.Migration -> {
|
|
||||||
MigrationScreenView(isMigrating = true)
|
|
||||||
}
|
|
||||||
is RoomListContentState.Skeleton -> {
|
is RoomListContentState.Skeleton -> {
|
||||||
SkeletonView(
|
SkeletonView(
|
||||||
count = contentState.count,
|
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